Общие рекомендации по предотвращению утечек памяти в C ++ [закрыто]

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

Вопрос

Каковы некоторые общие советы, чтобы убедиться, что я не допускаю утечки памяти в программах на C ++?Как мне определить, кто должен освобождать память, которая была динамически выделена?

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

Решение

Вместо того чтобы управлять памятью вручную, попробуйте использовать интеллектуальные указатели там, где это применимо.
Взгляните на Увеличить библиотеку, TR1, и умные указатели.
Кроме того, интеллектуальные указатели теперь являются частью стандарта C ++, называемого C++11.

Другие советы

Я полностью поддерживаю все советы по RAII и интеллектуальным указателям, но я также хотел бы добавить совет немного более высокого уровня:самая простая в управлении память - это та, которую вы никогда не выделяли.В отличие от таких языков, как C # и Java, где практически все является ссылкой, в C ++ вы должны помещать объекты в стек всякий раз, когда можете.Как я видел, несколько человек (включая доктора Страуструпа) отмечают, что основная причина, по которой сборка мусора никогда не была популярна в C ++, заключается в том, что хорошо написанный C ++ вообще не производит много мусора.

Не пиши

Object* x = new Object;

или даже

shared_ptr<Object> x(new Object);

когда ты можешь просто писать

Object x;

Использование РАЙИ

  • Забудьте О Сборе мусора (Вместо этого используйте RAII).Обратите внимание, что даже сборщик мусора тоже может протекать (если вы забудете "обнулить" некоторые ссылки в Java / C #), и этот сборщик мусора не поможет вам избавиться от ресурсов (если у вас есть объект, который получил дескриптор файла, файл не будет освобожден автоматически, когда объект выйдет за пределы области видимости, если вы не сделаете это вручную в Java или не используете шаблон "dispose" в C #).
  • Забудьте о правиле "один возврат для каждой функции".Это хороший совет C, позволяющий избежать утечек, но в C ++ он устарел из-за использования исключений (вместо этого используйте RAII).
  • И в то время как "сэндвич-шаблон" это хороший совет C, он устарел в C ++ из-за использования исключений (вместо этого используйте RAII).

Этот пост кажется повторяющимся, но в C ++ самый простой шаблон, который нужно знать, это РАЙИ.

Научитесь использовать интеллектуальные указатели, как из boost, TR1, так и даже из непритязательного (но часто достаточно эффективного) auto_ptr (но вы должны знать его ограничения).

RAII является основой как безопасности исключений, так и удаления ресурсов в C ++, и никакой другой шаблон (сэндвич и т.д.) Не даст вам и того, и другого (и в большинстве случаев он не даст вам ничего).

Смотрите ниже сравнение кода RAII и кода, отличного от RAII:

void doSandwich()
{
   T * p = new T() ;
   // do something with p
   delete p ; // leak if the p processing throws or return
}

void doRAIIDynamic()
{
   std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

void doRAIIStatic()
{
   T p ;
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

О нас РАЙИ

Подводя итог (после комментария от Людоедский Псалом 33), RAII опирается на три концепции:

  • Как только объект создан, он просто работает! Приобретайте ресурсы в конструкторе.
  • Уничтожения объекта достаточно! Освободите ресурсы в деструкторе.
  • Все дело в масштабах! Объекты с ограниченной областью действия (см. doRAIIStatic пример выше) будут созданы при их объявлении и будут уничтожены в тот момент, когда выполнение выйдет из области действия, независимо от способа выхода (возврат, разрыв, исключение и т.д.).

Это означает, что в правильном коде C ++ большинство объектов не будут создаваться с помощью new, и вместо этого будет объявлен в стеке.И для тех, которые построены с использованием new, все будет как - нибудь охватываемый (например,прикрепленный к интеллектуальному указателю).

Как разработчик, это действительно очень мощно, поскольку вам не нужно будет заботиться о ручной обработке ресурсов (как это делается в C или для некоторых объектов в Java, которые интенсивно используют try/finally для этого случая)...

Редактировать (2012-02-12)

"объекты с ограниченным охватом ...будет уничтожен ...независимо от выхода", это не совсем так.есть способы обмануть RAII.любой вариант terminate() обойдет очистку.exit(EXIT_SUCCESS) является оксюмороном в этом отношении.

wilhelmtell

wilhelmtell в этом он совершенно прав:Есть такие исключительный способы обмана RAII, все ведущие к резкой остановке процесса.

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

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

(кто звонит terminate или exit в обычном коде на C ++?...Я помню, что мне приходилось сталкиваться с этой проблемой, когда я играл с ПЕРЕНАСЫЩЕНИЕ:Эта библиотека очень ориентирована на C, вплоть до активного ее проектирования, чтобы усложнить задачу разработчикам C ++, например, не заботиться о стек распределенных данных, или принятие "интересных" решений о никогда не возвращаются из своего основного цикла...Я не буду это комментировать).

Вам захочется ознакомиться с умными указателями, такими как умные указатели boost.

Вместо того, чтобы

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

boost:: shared_ptr автоматически удалится, как только количество ссылок станет равным нулю:

int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

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

Однако это не панацея.Хотя вы можете получить доступ к базовому указателю, вы бы не захотели передавать его стороннему API, если бы не были уверены в том, что он делает.Часто вы "отправляете" материал в какой-то другой поток для выполнения работы ПОСЛЕ завершения создания области.Это обычное явление для PostThreadMessage в Win32:

void foo()
{
   boost::shared_ptr<Object> obj(new Object()); 

   // Simplified here
   PostThreadMessage(...., (LPARAM)ob.get());
   // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}

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

Читайте дальше РАЙИ и убедитесь, что вы это понимаете.

Большинство утечек памяти являются результатом неясности в отношении владения объектом и срока службы.

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

Если вам действительно нужно "создать" объект, то большую часть времени у него будет один очевидный владелец на всю оставшуюся жизнь.Для этой ситуации я обычно использую набор шаблонов коллекций, которые предназначены для "владения" объектами, хранящимися в них по указателю.Они реализованы с помощью контейнеров STL vector и map, но имеют некоторые отличия:

  • Эти коллекции не могут быть скопированы или назначены другим пользователям.(как только они содержат объекты.)
  • В них вставляются указатели на объекты.
  • Когда коллекция удаляется, деструктор сначала вызывается для всех объектов в коллекции.(У меня есть другая версия, где он утверждает, что он уничтожен, а не пуст.)
  • Поскольку они хранят указатели, вы также можете хранить унаследованные объекты в этих контейнерах.

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

Ба, вы, молодые ребята, и ваши новомодные мусорщики...

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

create a thing
use that thing
destroy that thing

Иногда необходимо создавать и разрушать в самых разных местах;я думаю, что трудно избежать этого.

В любой программе, требующей сложных структур данных, я создаю строгое четкое дерево объектов, содержащих другие объекты, используя указатели "владельца".Это дерево моделирует базовую иерархию концепций предметной области приложения.Пример: 3D-сцена владеет объектами, источниками света, текстурами.В конце рендеринга, когда программа завершает работу, есть очевидный способ уничтожить все.

Многие другие указатели определяются по мере необходимости всякий раз, когда одному объекту требуется доступ к другому, для сканирования через arays или что-то еще;это те, кто "просто смотрит".Для примера 3D-сцены - объект использует текстуру, но не владеет;другие объекты могут использовать ту же текстуру.Уничтожение объекта не приводит к нет вызовите разрушение любых текстур.

Да, это отнимает много времени, но это то, что я делаю.У меня редко возникают утечки памяти или другие проблемы.Но тогда я работаю в ограниченной сфере высокопроизводительного научного программного обеспечения, сбора данных и графического обеспечения.Я не часто имею дело с транзакциями, как в банковском деле и электронной коммерции, графическими интерфейсами, управляемыми событиями, или высокосетевым асинхронным хаосом.Может быть, у новомодных способов есть здесь преимущество!

Отличный вопрос!

если вы используете c ++ и разрабатываете приложение для работы с процессором и памятью в реальном времени (например, игры), вам необходимо написать свой собственный менеджер памяти.

Я думаю, что лучшее, что вы можете сделать, это объединить несколько интересных работ разных авторов, я могу дать вам несколько подсказок:

  • Распределитель фиксированного размера широко обсуждается повсюду в сети

  • Выделение небольших объектов было введено Александреску в 2001 году в его замечательной книге "Современный дизайн c ++".

  • Большое улучшение (с распространяемым исходным кодом) можно найти в потрясающей статье в Game Programming Gem 7 (2008) под названием "Высокопроизводительный распределитель кучи", написанной Димитром Лазаровым

  • Большой список ресурсов можно найти в это Статья

Не начинайте самостоятельно писать бесполезный распределитель для новичков...Сначала ЗАДОКУМЕНТИРУЙТЕ СЕБЯ.

Одним из методов, который стал популярным при управлении памятью в C ++, является РАЙИ.По сути, вы используете конструкторы / деструкторы для обработки распределения ресурсов.Конечно, в C ++ есть некоторые другие неприятные детали из-за безопасности исключений, но основная идея довольно проста.

Вопрос, как правило, сводится к вопросу о собственности.Я настоятельно рекомендую прочитать серию "Эффективный C ++" Скотта Мейерса и "Современный дизайн C ++" Андрея Александреску.

Уже много сказано о том, как избежать утечек, но если вам нужен инструмент, который поможет вам отслеживать утечки, взгляните на:

Умные указатели пользователей повсюду, где только можно!Целые классы утечек памяти просто исчезают.

Делитесь правилами владения памятью в рамках вашего проекта и знайте их.Использование правил COM обеспечивает наилучшую согласованность (параметры [in] принадлежат вызывающему, вызываемый должен скопировать;параметры [out] принадлежат вызывающему абоненту, вызываемый абонент должен сделать копию, если сохраняет ссылку;и т.д.)

валгринд это также хороший инструмент для проверки утечек памяти ваших программ во время выполнения.

Он доступен на большинстве версий Linux (включая Android) и на Darwin.

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

Конечно, этот совет остается в силе для любого другого инструмента проверки памяти.

Кроме того, не используйте выделенную вручную память, если существует класс библиотеки std (напримервектор).Убедитесь, что в случае нарушения этого правила у вас есть виртуальный деструктор.

Если вы не можете / не используете интеллектуальный указатель для чего-либо (хотя это должно быть огромным красным флагом), введите свой код с помощью:

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

Это очевидно, но обязательно введите его до того, как вы вводите любой код в область видимости

Частым источником этих ошибок является использование метода, который принимает ссылку или указатель на объект, но оставляет неясным право собственности.Соглашения о стиле и комментариях могут сделать это менее вероятным.

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

Использование const может очень помочь в некоторых случаях.Если функция не изменяет объект и не сохраняет ссылку на него, которая сохраняется после ее возврата, примите ссылку const .Из чтения кода вызывающего объекта будет очевидно, что ваша функция не приняла права собственности на объект.Вы могли бы заставить ту же функцию принять неконстантный указатель, и вызывающий объект мог предположить, а мог и не предположить, что вызываемый объект принял право собственности, но с постоянной ссылкой вопросов нет.

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

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

Советы в порядке важности:

-Совет № 1 Всегда помните объявлять свои деструкторы "виртуальными".

-Совет №2 Используйте RAII

-Совет № 3 Используйте смарт-указатели boost

-Совет № 4 Не пишите свои собственные глючные смарт-указатели, используйте boost (в проекте, над которым я сейчас работаю, я не могу использовать boost, и мне пришлось отлаживать свои собственные смарт-указатели, я бы определенно не пошел по тому же маршруту снова, но опять же, прямо сейчас я не могу добавить boost к нашим зависимостям)

-Совет № 5 Если это какая-то случайная / не критичная к производительности работа (как в играх с тысячами объектов), посмотрите на контейнер boost pointer от Торстена Оттозена

-Совет № 6 Найдите заголовок обнаружения утечек для выбранной вами платформы, например заголовок визуального обнаружения утечек "vld"

Если вы можете, используйте boost shared_ptr и стандартный C ++ auto_ptr.Они передают семантику владения.

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

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

Эта семантика также применима к параметрам.Если вызывающий передает вам auto_ptr, он передает вам право собственности.

Другие упоминали способы избежать утечек памяти в первую очередь (например, интеллектуальные указатели).Но инструмент профилирования и анализа памяти часто является единственным способом выявить проблемы с памятью, как только они у вас возникнут.

Проверка памяти Valgrind это отличный бесплатный сервис.

Только для MSVC добавьте следующее в начало каждого файла .cpp:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Затем, при отладке с помощью VS2003 или более поздней версии, вам будет сообщено о любых утечках при завершении работы вашей программы (она отслеживает создание / удаление).Это элементарно, но в прошлом это помогало мне.

valgrind (доступен только для платформ * nix) - очень хорошая программа для проверки памяти

Если вы собираетесь управлять своей памятью вручную, у вас есть два варианта:

  1. Я создал объект (возможно, косвенно, вызвав функцию, которая выделяет новый объект), я использую его (или вызываемая мной функция использует его), затем я освобождаю его.
  2. Кто-то дал мне ссылку, так что я не должен ее освобождать.

Если вам нужно нарушить какое-либо из этих правил, пожалуйста, задокументируйте это.

Все дело в владении указателем.

  • Старайтесь избегать динамического выделения объектов.Пока классы имеют соответствующие конструкторы и деструкторы, используйте переменную типа class, а не указатель на нее, и вы избежите динамического выделения и освобождения, потому что компилятор сделает это за вас.
    На самом деле это также механизм, используемый "умными указателями" и называемый RAII некоторыми другими авторами ;-) .
  • Когда вы передаете объекты другим функциям, отдавайте предпочтение ссылочным параметрам, а не указателям.Это позволяет избежать некоторых возможных ошибок.
  • Объявляйте параметры const, где это возможно, особенно указатели на объекты.Таким образом, объекты не могут быть освобождены "случайно" (за исключением случаев, когда вы отбрасываете const ;-))).
  • Сведите к минимуму количество мест в программе, где вы выполняете выделение и освобождение памяти.E.g.если вы выделяете или освобождаете один и тот же тип несколько раз, напишите для него функцию (или фабричный метод ;-)).
    Таким образом, вы можете легко создать отладочный вывод (какие адреса выделяются и освобождаются, ...), если требуется.
  • Используйте фабричную функцию для выделения объектов нескольких связанных классов из одной функции.
  • Если ваши классы имеют общий базовый класс с виртуальным деструктором, вы можете освободить их все, используя одну и ту же функцию (или статический метод).
  • Проверьте свою программу с помощью таких инструментов, как purify (к сожалению, много $ / € / ...).

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

Это также можно сделать во время компиляции, заменив операторы new и delete и другие функции выделения памяти.

Например, проверьте это Сайт [Отладка выделения памяти в C ++] Примечание:Существует трюк для оператора удаления, также что-то вроде этого:

#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE

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

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

Мы оборачиваем все наши функции распределения слоем, который добавляет короткую строку спереди и флаг-указатель в конце.Так, например, у вас будет вызов "myalloc( pszSomeString, iSize, iAlignment );или создать ( "описание", iSize) MyObject();который внутренне выделяет указанный размер плюс достаточно места для вашего заголовка и sentinel.Конечно, не забудьте прокомментировать это для не отладочных сборок!Для этого требуется немного больше памяти, но преимущества намного перевешивают затраты.

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

C ++ разработан с учетом RAII.Я думаю, что на C ++ действительно нет лучшего способа управления памятью.Но будьте осторожны, чтобы не выделять очень большие куски (например, буферные объекты) в локальной области видимости.Это может привести к переполнению стека, и, если при использовании этого фрагмента есть ошибка в проверке границ, вы можете перезаписать другие переменные или адреса возврата, что приводит ко всевозможным дырам в безопасности.

Одним из единственных примеров выделения и уничтожения в разных местах является создание потока (передаваемый вами параметр).Но даже в этом случае это несложно.Вот функция / метод, создающий поток:

struct myparams {
int x;
std::vector<double> z;
}

std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...

Здесь вместо этого функция thread

extern "C" void* th_func(void* p) {
   try {
       std::auto_ptr<myparams> param((myparams*)p);
       ...
   } catch(...) {
   }
   return 0;
}

Довольно просто, не правда ли?В случае сбоя при создании потока ресурс будет освобожден (удален) с помощью auto_ptr, в противном случае право собственности будет передано потоку.Что, если поток настолько быстр, что после создания он освобождает ресурс до того, как

param.release();

вызывается в основной функции / методе?Ничего!Потому что мы "скажем" auto_ptr игнорировать освобождение.Легко ли управлять памятью на C ++, не так ли?Ваше здоровье,

Эма!

Управляйте памятью так же, как вы управляете другими ресурсами (дескрипторами, файлами, подключениями к БД, сокетами ...).GC тоже не поможет вам с ними.

Ровно один возврат из любой функции.Таким образом, вы можете сделать освобождение там и никогда не пропустить его.

В противном случае слишком легко совершить ошибку:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top