Каковы общие советы, позволяющие избежать утечки памяти в программах на C ++? Как мне определить, кто должен освобождать динамически выделенную память?
Каковы общие советы, позволяющие избежать утечки памяти в программах на C ++? Как мне определить, кто должен освобождать динамически выделенную память?
Ответы:
Вместо того, чтобы управлять памятью вручную, попробуйте по возможности использовать интеллектуальные указатели.
Взгляните на Boost lib , TR1 и интеллектуальные указатели .
Также интеллектуальные указатели теперь являются частью стандарта C ++ под названием C ++ 11 .
Я полностью поддерживаю все советы по поводу RAII и интеллектуальных указателей, но я также хотел бы добавить немного более высокий совет: проще всего управлять памятью, которую вы никогда не выделяли. В отличие от таких языков, как C # и Java, где практически все является ссылкой, в C ++ вы должны помещать объекты в стек всякий раз, когда можете. Как я видел, как указывают несколько человек (включая доктора Страуструпа), основная причина, по которой сборка мусора никогда не была популярной в C ++, заключается в том, что хорошо написанный C ++ изначально не производит большого количества мусора.
Не пиши
Object* x = new Object;
или даже
shared_ptr<Object> x(new Object);
когда ты можешь просто написать
Object x;
Этот пост кажется повторяющимся, но в 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.
}
Подводя итог (после комментария Ogre Psalm33 ), RAII опирается на три концепции:
Это означает, что в правильном коде C ++ большинство объектов не будут построены с использованием new
и вместо этого будут объявлены в стеке. А для созданных с использованием new
все будет как-то ограничено (например, прикреплено к интеллектуальному указателю).
Как разработчик, это действительно очень эффективно, поскольку вам не нужно заботиться о ручной обработке ресурсов (как это сделано в C или для некоторых объектов в Java, которые интенсивно используют try
/ finally
в этом случае) ...
«объекты с заданной областью ... будут разрушены ... независимо от выхода», что не совсем так. есть способы обмануть RAII. любой вариант terminate () будет обходить очистку. exit (EXIT_SUCCESS) - оксюморон в этом отношении.
Вильгельмтелл совершенно прав: существуют исключительные способы обмануть RAII, и все они приводят к внезапной остановке процесса.
Это исключительные способы, потому что код C ++ не загроможден символами завершения, выхода и т. Д., Или, в случае с исключениями, мы действительно хотим, чтобы необработанное исключение приводило к сбою процесса и дампу основного образа памяти как есть, а не после очистки.
Но мы все равно должны знать об этих случаях, потому что, хотя они случаются редко, они все же могут случиться.
(кто вызывает terminate
или exit
в случайном коде C ++? ... Я помню, что имел дело с этой проблемой, играя с GLUT : эта библиотека очень ориентирована на 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, но имеют некоторые отличия:
Мне нравится STL, поскольку он настолько сфокусирован на объектах Value, в то время как в большинстве приложений объекты являются уникальными сущностями, не имеющими смысловой семантики копирования, необходимой для использования в этих контейнерах.
Ба, вы, молодые ребята, и ваши новомодные сборщики мусора ...
Очень строгие правила относительно «владения» - какой объект или часть программы имеет право удалить объект. Четкие комментарии и мудрые имена переменных, чтобы было очевидно, «владеет» ли указатель или «просто смотри, не трогай». Чтобы решить, кому что принадлежит, как можно больше следуйте шаблону «сэндвич» в каждой подпрограмме или методе.
create a thing
use that thing
destroy that thing
Иногда необходимо создавать и разрушать в самых разных местах; Я очень стараюсь этого избежать.
В любой программе, требующей сложных структур данных, я создаю строго очерченное дерево объектов, содержащих другие объекты, используя указатели «владельца». Это дерево моделирует базовую иерархию концепций предметной области. Пример: 3D-сцена содержит объекты, источники света, текстуры. В конце рендеринга, когда программа завершает работу, есть четкий способ все уничтожить.
Многие другие указатели определяются по мере необходимости всякий раз, когда одной сущности требуется доступ к другой, для сканирования лучей или чего-то еще; это «просто ищущие». Для примера 3D сцены - объект использует текстуру, но не владеет ею; другие объекты могут использовать ту же текстуру. Разрушение объекта не влечет за собой разрушение каких-либо текстур.
Да, это требует времени, но я этим занимаюсь. У меня редко бывают утечки памяти или другие проблемы. Но затем я работаю в ограниченной области высокопроизводительного программного обеспечения для научных исследований, сбора данных и графики. Я не часто занимаюсь транзакциями, например, в банковском деле и электронной коммерции, с графическими интерфейсами, управляемыми событиями, или с высоким уровнем сетевого асинхронного хаоса. Может быть, у новомодных способов есть преимущество!
Отличный вопрос!
Если вы используете C ++ и разрабатываете приложение для работы с процессором и памятью в реальном времени (например, игры), вам необходимо написать собственный диспетчер памяти.
Я думаю, что лучше объединить несколько интересных работ разных авторов, я могу вам подсказать:
Распределитель фиксированного размера широко обсуждается повсюду в сети.
Размещение малых объектов было введено Александреску в 2001 году в его прекрасной книге «Современный дизайн c ++».
Большой прогресс (с распространенным исходным кодом) можно найти в замечательной статье в Game Programming Gem 7 (2008) под названием «High Performance Heap allocator», написанной Димитаром Лазаровым.
Большой список ресурсов можно найти в этой статье
Не начинайте самостоятельно писать бесполезный распределитель памяти ... сначала ДОКУМЕНТУЙТЕ СЕБЯ.
Один из методов, который стал популярным при управлении памятью в C ++, - это RAII . В основном вы используете конструкторы / деструкторы для обработки распределения ресурсов. Конечно, в C ++ есть и другие неприятные детали из-за безопасности исключений, но основная идея довольно проста.
Проблема обычно сводится к одному из владельцев. Я настоятельно рекомендую прочитать серию «Эффективный C ++» Скотта Мейерса и «Современный дизайн C ++» Андрея Александреску.
Уже есть много о том, как избежать утечек, но если вам нужен инструмент, который поможет вам отслеживать утечки, обратите внимание на:
Делитесь и знайте правила владения памятью в вашем проекте. Использование правил COM обеспечивает наилучшую согласованность (параметры [in] принадлежат вызывающему, вызываемый должен копировать; параметры [out] принадлежат вызывающему, вызываемый должен делать копию, если сохраняет ссылку и т. Д.)
valgrind также является хорошим инструментом для проверки утечек памяти ваших программ во время выполнения.
Он доступен на большинстве разновидностей Linux (включая Android) и на Darwin.
Если вы используете для написания модульных тестов для своих программ, вы должны иметь привычку систематически запускать valgrind для тестов. Это потенциально позволит избежать многих утечек памяти на ранней стадии. Также обычно легче определить их в простых тестах, чем в полном программном обеспечении.
Конечно, этот совет остается в силе для любого другого инструмента проверки памяти.
Если вы не можете / не можете использовать интеллектуальный указатель для чего-либо (хотя это должен быть огромный красный флаг), введите свой код с помощью:
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.