Какие общие советы помогут избежать утечки памяти в программах на C ++? Как мне выяснить, кто должен освобождать память, которая была выделена динамически?
Общие рекомендации по предотвращению утечек памяти в C ++
Ответы (28)
Вместо того, чтобы управлять памятью вручную, попробуйте по возможности использовать интеллектуальные указатели.
Взгляните на Boost lib, TR1 и интеллектуальные указатели.
Также интеллектуальные указатели теперь являются частью стандарта C ++ под названием C ++ 11.
Я полностью поддерживаю все советы по поводу RAII и интеллектуальных указателей, но я также хотел бы добавить немного более высокий совет: проще всего управлять памятью, которую вы никогда не выделяли. В отличие от таких языков, как C # и Java, где практически все является ссылкой, в C ++ вы должны помещать объекты в стек всякий раз, когда это возможно. Как я видел, как указывают несколько человек (включая доктора Страуструпа), основная причина, по которой сборка мусора никогда не была популярной в C ++, заключается в том, что хорошо написанный C ++ изначально не производит большого количества мусора.
Не пиши
Object* x = new Object;
или даже
shared_ptr<Object> x(new Object);
когда ты можешь просто написать
Object x;
x
выделяется в стеке, означает ли это, что если вы передадите объект x
в качестве параметра, то весь объект будет скопирован?
- person Robert; 12.10.2014
std::array
, вам будет не по себе. Но ладно, это крайние случаи
- person Jean-Bernard Jansen; 03.12.2015
x
в стеке, вы можете (и часто должны) писать функции, которые принимают его по ссылке. (Конечно, вы не можете вернуть его по ссылке; но оптимизация возвращаемого значения означает, что возврат по значению также не обязательно предполагает получение копии.)
- person ruakh; 12.06.2016
Используйте RAII
- Забудьте о сборке мусора (используйте вместо этого RAII). Обратите внимание, что даже сборщик мусора тоже может протекать (если вы забыли «обнулить» некоторые ссылки в Java / C #), и этот сборщик мусора не поможет вам избавиться от ресурсов (если у вас есть объект, который получил дескриптор для файл не будет освобожден автоматически, когда объект выйдет за пределы области видимости, если вы не сделаете это вручную в Java или не воспользуетесь шаблоном «dispose» в C #).
- Забудьте о правиле «один возврат на функцию». Это хороший совет C, чтобы избежать утечек, но он устарел в C ++ из-за использования исключений (вместо этого используйте RAII).
- И хотя «Сэндвич-шаблон» является хорошим советом для C, он устарел в C ++ из-за использования исключений (вместо этого используйте RAII).
Этот пост кажется повторяющимся, но в C ++ самый простой шаблон, который нужно знать, - это RAII.
Научитесь использовать интеллектуальные указатели, как из 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.
}
О RAII
Подводя итог (после комментария из Ogre Psalm33), RAII опирается на три концепции:
- Как только объект построен, он просто работает! Получите ресурсы в конструкторе.
- Достаточно разрушения объекта! Освободите ресурсы в деструкторе.
- Все дело в областях! Объекты с заданной областью (см. пример doRAIIStatic выше) будут созданы при их объявлении и будут уничтожены в момент выхода выполнения из области действия, независимо от того, как выход (возврат, прерывание, исключение , и т.д.).
Это означает, что в правильном коде C ++ большинство объектов не будут построены с new
, а вместо этого будут объявлены в стеке. А для тех, которые созданы с использованием new
, все будут каким-то образом ограничены (например, прикреплены к интеллектуальному указателю).
Как разработчик, это действительно очень эффективно, поскольку вам не нужно заботиться о ручной обработке ресурсов (как это сделано в C или для некоторых объектов в Java, которые интенсивно используют _4 _ / _ 5_ для этого случая) ...
Изменить (2012-02-12)
"объекты с заданной областью ... будут разрушены ... независимо от выхода", что не совсем так. есть способы обмануть RAII. любой вариант terminate () будет обходить очистку. exit (EXIT_SUCCESS) - оксюморон в этом отношении.
Wilhelmtell совершенно прав в этом: существуют исключительные способы обмануть RAII, и все они приводят к резкая остановка процесса.
Это исключительные способы, потому что код C ++ не загроможден символами завершения, выхода и т. Д., Или, в случае с исключениями, нам действительно нужен необработанное исключение для аварийного завершения процесса и создания дампа образа памяти как есть, а не после очистки.
Но мы все равно должны знать об этих случаях, потому что, хотя они случаются редко, они все же могут случиться.
(кто вызывает terminate
или exit
в обычном коде C ++? ... Я помню, как мне приходилось сталкиваться с этой проблемой, играя с GLUT: эта библиотека очень ориентирована на C, вплоть до того, что активно разрабатывала ее, чтобы усложнить задачу разработчикам C ++, например, не заботиться о выделенные в стеке данные или наличие" интересных "решений по никогда не возвращаются из основного цикла ... Я не буду это комментировать).
terminate()
будет обходить очистку. exit(EXIT_SUCCESS)
- оксюморон в этом отношении.
- person wilhelmtell; 14.02.2012
doRAIIDynamic
vrs doRAIIStatic
? Правильно ли я думаю, что статический регистр размещается в куче, поэтому, если объект T
большой, вы захотите использовать динамическую версию?
- person Robert; 12.10.2014
Вы захотите взглянуть на интеллектуальные указатели, такие как интеллектуальные указатели 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!
}
Как всегда, используйте свою мыслящую шапку с любым инструментом ...
Ба, вы, молодые ребята, и ваши новомодные сборщики мусора ...
Очень строгие правила относительно «владения» - какой объект или часть программы имеет право удалить объект. Четкие комментарии и мудрые имена переменных, чтобы было очевидно, «владеет» ли указатель или «просто смотри, не трогай». Чтобы помочь решить, кому что принадлежит, следуйте как можно более шаблону «сэндвич» в каждой подпрограмме или методе.
create a thing
use that thing
destroy that thing
Иногда необходимо создавать и разрушать в самых разных местах; Я очень стараюсь этого избежать.
В любой программе, требующей сложных структур данных, я создаю строго очерченное дерево объектов, содержащих другие объекты, используя указатели «владельца». Это дерево моделирует базовую иерархию концепций предметной области. Пример: 3D-сцена содержит объекты, источники света, текстуры. В конце рендеринга, когда программа завершает работу, есть четкий способ все уничтожить.
Многие другие указатели определяются по мере необходимости, когда одному объекту нужен доступ к другому, для сканирования лучей или чего-то еще; это «просто ищущие». В примере с 3D-сценой - объект использует текстуру, но не владеет ею; другие объекты могут использовать ту же текстуру. Уничтожение объекта не вызывает разрушение каких-либо текстур.
Да, это требует времени, но я этим занимаюсь. У меня редко бывают утечки памяти или другие проблемы. Но затем я работаю в ограниченной сфере высокопроизводительного программного обеспечения для научных исследований, сбора данных и графики. Я не часто занимаюсь транзакциями, например, в банковском деле и электронной коммерции, с графическим интерфейсом, управляемым событиями, или с высоким уровнем сетевого асинхронного хаоса. Может быть, у новомодных способов есть преимущество!
Большинство утечек памяти являются результатом непонятного отношения к объекту собственности и времени жизни.
Первое, что нужно сделать, - это разместить в стеке всякий раз, когда это возможно. Это касается большинства случаев, когда вам нужно выделить один объект для какой-либо цели.
Если вам действительно нужно «создать новый» объект, то в большинстве случаев у него будет один очевидный владелец на всю оставшуюся жизнь. В этой ситуации я обычно использую набор шаблонов коллекций, которые предназначены для «владения» объектами, хранящимися в них, по указателю. Они реализованы с помощью контейнеров векторов и карт STL, но имеют некоторые отличия:
- Эти коллекции нельзя копировать или назначать. (если они содержат объекты.)
- В них вставляются указатели на объекты.
- Когда коллекция удаляется, деструктор сначала вызывается для всех объектов в коллекции. (У меня есть другая версия, в которой утверждается, что она разрушена, а не пуста.)
- Поскольку они хранят указатели, вы также можете хранить в этих контейнерах унаследованные объекты.
Мне нравится STL, поскольку он так сфокусирован на объектах Value, в то время как в большинстве приложений объекты являются уникальными сущностями, не имеющими смысловой семантики копирования, необходимой для использования в этих контейнерах.
Отличный вопрос!
Если вы используете C ++ и разрабатываете приложение для работы с процессором и памятью в реальном времени (например, игры), вам необходимо написать свой собственный диспетчер памяти.
Я думаю, что лучше объединить несколько интересных работ разных авторов, я могу вам подсказать:
Распределитель фиксированного размера широко обсуждается повсюду в сети.
Размещение малых объектов было введено Александреску в 2001 году в его прекрасной книге «Современный дизайн на C ++».
Большой прогресс (с распределенным исходным кодом) можно найти в замечательной статье в Game Programming Gem 7 (2008) под названием «High Performance Heap allocator», написанной Димитаром Лазаровым.
Большой список ресурсов можно найти в этом а> статья
Не начинайте самостоятельно писать бесполезный распределитель памяти ... сначала ДОКУМЕНТУЙТЕ СЕБЯ.
Один из методов, который стал популярным при управлении памятью в C ++, - это RAII. В основном вы используете конструкторы / деструкторы для обработки распределения ресурсов. Конечно, в C ++ есть и другие неприятные детали из-за безопасности исключений, но основная идея довольно проста.
Проблема обычно сводится к одному из владельцев. Я настоятельно рекомендую прочитать серию «Эффективный C ++» Скотта Мейерса и «Современный дизайн C ++» Андрея Александреску.
Уже есть много о том, как избежать утечек, но если вам нужен инструмент, который поможет вам отслеживать утечки, обратите внимание на:
- BoundsChecker в VS
- Библиотека MMGR C / C ++ из FluidStudio http://www.paulnettle.com/pub/FluidStudios/MemoryManagers/Fluid_Studios_Memory_Manager.zip (переопределяет методы распределения и создает отчет о выделениях, утечках и т. Д.)
Пользовательские умные указатели везде, где только можно! Целые классы утечек памяти просто исчезают.
Делитесь и знайте правила владения памятью в вашем проекте. Использование правил COM обеспечивает наилучшую согласованность (параметры [in] принадлежат вызывающему, вызываемый должен копировать; параметры [out] принадлежат вызывающему, вызываемый должен делать копию, если сохраняет ссылку и т. Д.)
valgrind - хороший инструмент для проверки утечек памяти в ваших программах во время выполнения.
Он доступен на большинстве разновидностей Linux (включая Android) и на Darwin.
Если вы используете для написания модульных тестов для своих программ, вы должны иметь привычку систематически запускать valgrind для тестов. Это потенциально позволит избежать многих утечек памяти на ранней стадии. Кроме того, обычно легче определить их в простых тестах, чем в полном программном обеспечении.
Конечно, этот совет остается в силе и для любого другого инструмента проверки памяти.
Кроме того, не используйте выделенную вручную память, если есть класс библиотеки std (например, вектор). Если вы нарушите это правило, убедитесь, что у вас есть виртуальный деструктор.
Если вы не можете / не можете использовать интеллектуальный указатель для чего-либо (хотя это должен быть огромный красный флаг), введите свой код с помощью:
allocate
if allocation succeeded:
{ //scope)
deallocate()
}
Это очевидно, но убедитесь, что вы набираете его перед вводом любого кода в области видимости.
Частый источник этих ошибок - это когда у вас есть метод, который принимает ссылку или указатель на объект, но оставляет неясным право собственности. Соглашения о стилях и комментариях могут снизить вероятность этого.
Пусть случай, когда функция становится владельцем объекта, будет особым случаем. Во всех ситуациях, когда это происходит, обязательно напишите комментарий рядом с функцией в файле заголовка, указывающий на это. Вы должны стремиться к тому, чтобы в большинстве случаев модуль или класс, выделяющий объект, также отвечал за его освобождение.
В некоторых случаях использование const может очень помочь. Если функция не будет изменять объект и не хранит ссылку на него, которая сохраняется после его возврата, примите константную ссылку. Из чтения кода вызывающего будет очевидно, что ваша функция не приняла владение объектом. У вас могла бы быть одна и та же функция, принимающая неконстантный указатель, и вызывающая сторона могла или не могла предположить, что вызываемая сторона приняла право владения, но с константной ссылкой в этом нет никаких сомнений.
Не используйте неконстантные ссылки в списках аргументов. При чтении кода вызывающего абонента очень неясно, мог ли вызываемый объект сохранить ссылку на параметр.
Я не согласен с комментариями, в которых рекомендуются указатели с подсчетом ссылок. Обычно это работает нормально, но когда у вас есть ошибка, и она не работает, особенно если ваш деструктор делает что-то нетривиальное, например, в многопоточной программе. Обязательно попробуйте настроить свой дизайн так, чтобы он не нуждался в подсчете ссылок, если это не слишком сложно.
Советы в порядке важности:
-Совет №1 Всегда не забывайте объявлять деструкторы виртуальными.
-Совет № 2 Используйте RAII
-Совет № 3 Используйте смарт-указатели Boost
-Совет № 4 Не пишите свои собственные смарт-указатели с ошибками, используйте ускорение (в проекте, над которым я сейчас работаю, я не могу использовать ускорение, и мне пришлось отлаживать свои собственные интеллектуальные указатели, я бы определенно не стал тот же маршрут снова, но опять же, прямо сейчас я не могу добавить усиление нашим зависимостям)
-Совет №5. Если это некоторая случайная / не критичная для производительности (как в играх с тысячами объектов) работа, посмотрите на контейнер указателя ускорения Торстена Оттосена.
-Совет № 6 Найдите заголовок обнаружения утечек для выбранной платформы, например заголовок «vld» Visual Leak Detection.
Если можете, используйте boost shared_ptr и стандартный C ++ auto_ptr. Они передают семантику владения.
Когда вы возвращаете auto_ptr, вы сообщаете вызывающему, что передаете ему право владения памятью.
Когда вы возвращаете shared_ptr, вы говорите вызывающей стороне, что у вас есть ссылка на него, и они принимают на себя часть владения, но это не является их исключительной ответственностью.
Эта семантика также применима к параметрам. Если вызывающий передает вам auto_ptr, они передают вам право собственности.
Другие упоминали способы предотвращения утечек памяти в первую очередь (например, интеллектуальные указатели). Но инструмент профилирования и анализа памяти часто является единственным способом отследить проблемы с памятью, если они у вас возникнут.
Valgrind memcheck - отличная бесплатная программа.
Только для MSVC добавьте следующее в начало каждого файла .cpp:
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
Затем, при отладке с помощью VS2003 или более поздней версии, вам сообщат о любых утечках, когда ваша программа завершится (она отслеживает новые / удаляемые). Это элементарно, но в прошлом мне это помогало.
valgrind (только для платформ * nix) - очень хорошая программа проверки памяти
Если вы собираетесь управлять своей памятью вручную, у вас есть два случая:
- Я создал объект (возможно, косвенно, вызвав функцию, которая выделяет новый объект), я использую его (или вызываемая мной функция использует его), а затем освобождаю его.
- Кто-то дал мне ссылку, поэтому я не должен ее освобождать.
Если вам нужно нарушить какое-либо из этих правил, пожалуйста, задокументируйте это.
Все дело в владении указателем.
Вы можете перехватить функции выделения памяти и посмотреть, есть ли какие-то зоны памяти, не освобожденные при выходе из программы (хотя это не подходит для всех приложений).
Это также можно сделать во время компиляции, заменив операторы new и delete и другие функции выделения памяти.
Например, посетите этот сайт [Отладка выделения памяти в C ++] Примечание. Есть еще одна хитрость с оператором удаления:
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE
Вы можете сохранить в некоторых переменных имя файла, и когда перегруженный оператор удаления будет знать, из какого места он был вызван. Таким образом, вы можете отслеживать каждое удаление и malloc из вашей программы. В конце последовательности проверки памяти вы должны иметь возможность сообщить, какой выделенный блок памяти не был «удален», идентифицируя его по имени файла и номеру строки, который, я думаю, вам нужен.
Вы также можете попробовать что-то вроде BoundsChecker в Visual Studio, что довольно интересно и просто. использовать.
Мы оборачиваем все наши функции распределения слоем, который добавляет короткую строку впереди и сигнальный флаг в конце. Так, например, у вас будет вызов myalloc (pszSomeString, iSize, iAlignment); или new («description», iSize) MyObject ();, который внутренне выделяет указанный размер плюс достаточно места для вашего заголовка и дозорного. Конечно , не забудьте прокомментировать это для неотладочных сборок! Для этого требуется немного больше памяти, но преимущества намного перевешивают затраты.
У этого есть три преимущества: во-первых, это позволяет вам легко и быстро отслеживать, какой код протекает, путем быстрого поиска кода, выделенного в определенных «зонах», но не очищенного, когда эти зоны должны были быть освобождены. Также может быть полезно определить, когда граница была перезаписана, путем проверки, чтобы убедиться, что все контрольные точки не повреждены. Это спасало нас много раз, когда мы пытались найти эти хорошо скрытые сбои или ошибки в массиве. Третье преимущество заключается в отслеживании использования памяти, чтобы увидеть, кто такие крупные игроки - сопоставление определенных описаний в 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();
...
Здесь вместо функции потока
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.