Как избежать утечек памяти при использовании вектора указателей на динамически выделяемые объекты в C ++?
Вопрос
Я использую вектор указателей на объекты.Эти объекты являются производными от базового класса и динамически распределяются и хранятся.
Например, у меня есть что-то вроде:
vector<Enemy*> Enemies;
и я буду извлекать данные из вражеского класса, а затем динамически выделять память для производного класса, вот так:
enemies.push_back(new Monster());
О каких вещах мне нужно знать, чтобы избежать утечек памяти и других проблем?
Решение
std::vector
будет управлять памятью за вас, как всегда, но эта память будет состоять из указателей, а не объектов.
Это означает, что ваши классы будут потеряны в памяти, как только ваш вектор выйдет за пределы области видимости.Например:
#include <vector>
struct base
{
virtual ~base() {}
};
struct derived : base {};
typedef std::vector<base*> container;
void foo()
{
container c;
for (unsigned i = 0; i < 100; ++i)
c.push_back(new derived());
} // leaks here! frees the pointers, doesn't delete them (nor should it)
int main()
{
foo();
}
Что вам нужно сделать, так это убедиться, что вы удалили все объекты до того, как вектор выйдет за пределы области видимости:
#include <algorithm>
#include <vector>
struct base
{
virtual ~base() {}
};
struct derived : base {};
typedef std::vector<base*> container;
template <typename T>
void delete_pointed_to(T* const ptr)
{
delete ptr;
}
void foo()
{
container c;
for (unsigned i = 0; i < 100; ++i)
c.push_back(new derived());
// free memory
std::for_each(c.begin(), c.end(), delete_pointed_to<base>);
}
int main()
{
foo();
}
Однако это сложно поддерживать, потому что нам нужно помнить о выполнении некоторых действий.Что еще более важно, если между выделением элементов и циклом освобождения произойдет исключение, цикл освобождения никогда не запустится, и вы все равно столкнетесь с утечкой памяти!Это называется безопасностью исключений, и это важная причина, по которой освобождение должно выполняться автоматически.
Лучше было бы, если бы указатели удалялись сами.Они называются интеллектуальными указателями, и стандартная библиотека предоставляет std::unique_ptr
и std::shared_ptr
.
std::unique_ptr
представляет собой уникальный (необщий, с одним владельцем) указатель на некоторый ресурс.Это должен быть ваш интеллектуальный указатель по умолчанию и полная замена любого использования необработанного указателя.
auto myresource = /*std::*/make_unique<derived>(); // won't leak, frees itself
std::make_unique
отсутствует в стандарте C++11 по недосмотру, но вы можете создать его самостоятельно.Чтобы непосредственно создать unique_ptr
(не рекомендуется более make_unique
если можете), сделайте следующее:
std::unique_ptr<derived> myresource(new derived());
Уникальные указатели имеют только семантику перемещения;их нельзя скопировать:
auto x = myresource; // error, cannot copy
auto y = std::move(myresource); // okay, now myresource is empty
И это все, что нам нужно, чтобы использовать его в контейнере:
#include <memory>
#include <vector>
struct base
{
virtual ~base() {}
};
struct derived : base {};
typedef std::vector<std::unique_ptr<base>> container;
void foo()
{
container c;
for (unsigned i = 0; i < 100; ++i)
c.push_back(make_unique<derived>());
} // all automatically freed here
int main()
{
foo();
}
shared_ptr
имеет семантику копирования с подсчетом ссылок;это позволяет нескольким владельцам совместно использовать объект.Он отслеживает, сколько shared_ptr
существуют для объекта, и когда последний из них перестает существовать (этот счетчик стремится к нулю), указатель освобождается.Копирование просто увеличивает количество ссылок (а перемещение передает право собственности с меньшими, почти бесплатными затратами).Вы делаете их с std::make_shared
(или напрямую, как показано выше, но поскольку shared_ptr
должен выполнять внутреннее распределение, его использование, как правило, более эффективно и технически более безопасно для исключений. make_shared
).
#include <memory>
#include <vector>
struct base
{
virtual ~base() {}
};
struct derived : base {};
typedef std::vector<std::shared_ptr<base>> container;
void foo()
{
container c;
for (unsigned i = 0; i < 100; ++i)
c.push_back(std::make_shared<derived>());
} // all automatically freed here
int main()
{
foo();
}
Помните, что обычно вы хотите использовать std::unique_ptr
по умолчанию, потому что он более легкий.Кроме того, std::shared_ptr
может быть построен из std::unique_ptr
(но не наоборот), поэтому можно начинать с малого.
В качестве альтернативы вы можете использовать контейнер, созданный для хранения указателей на объекты, например boost::ptr_container
:
#include <boost/ptr_container/ptr_vector.hpp>
struct base
{
virtual ~base() {}
};
struct derived : base {};
// hold pointers, specially
typedef boost::ptr_vector<base> container;
void foo()
{
container c;
for (int i = 0; i < 100; ++i)
c.push_back(new Derived());
} // all automatically freed here
int main()
{
foo();
}
Пока boost::ptr_vector<T>
имел очевидное применение в C++03, сейчас я не могу говорить об актуальности, поскольку мы можем использовать std::vector<std::unique_ptr<T>>
вероятно, с небольшими сопоставимыми накладными расходами или вообще без них, но это утверждение следует проверить.
Несмотря ни на что, никогда не освобождайте вещи явно в своем коде.Подведите итоги, чтобы убедиться, что управление ресурсами выполняется автоматически.В вашем коде не должно быть необработанных указателей владения.
По умолчанию в игре я бы, вероятно, выбрал std::vector<std::shared_ptr<T>>
.В любом случае мы ожидаем обмена, это происходит достаточно быстро, пока профилирование не скажет иное, это безопасно и легко в использовании.
Другие советы
Я предполагаю следующее:
- У вас есть вектор типа вектор< base* >
- Вы помещаете указатели на этот вектор после размещения объектов в куче.
- Вы хотите выполнить push_back указателя производного* в этот вектор.
Мне на ум приходят следующие вещи:
- Вектор не освобождает память объекта, на который указывает указатель.Вам придется удалить его самому.
- Ничего особенного для вектора, но деструктор базового класса должен быть виртуальным.
- вектор< base* > и вектор< производный* > — это два совершенно разных типа.
Проблема с использованием vector<T*>
заключается в том, что всякий раз, когда вектор неожиданно выходит за пределы области видимости (например, при возникновении исключения), вектор очищается после себя, но это освободит только память, которой он управляет для хранения указатель, а не память, которую вы выделили для того, на что ссылаются указатели.Итак Джи Мэн delete_pointed_to
функция имеет ограниченную ценность, так как работает только тогда, когда ничего не идет наперекосяк.
Все, что вам нужно сделать, это использовать интеллектуальный указатель:
vector< std::tr1::shared_ptr<Enemy> > Enemies;
(Если ваша std-библиотека поставляется без TR1, используйте boost::shared_ptr
вместо этого.)
За исключением очень редких угловых случаев (циклических ссылок), это просто устраняет проблему времени жизни объекта.
Редактировать:Обратите внимание, что GMan в своем подробном ответе тоже упоминает об этом.
Следует быть очень осторожным: ЕСЛИ есть два объекта Monster() DERIVED, содержимое которых идентично по значению.Предположим, вы хотите удалить объекты DUPLICATE Monster из вашего вектора (указатели BASE-класса на объекты DERIVED Monster).Если вы использовали стандартную идиому удаления дубликатов (сортировка, уникальность, стирание):см. ССЫЛКУ № 2], вы столкнетесь с проблемами утечки памяти и/или дублирующими проблемами удаления, что может привести к НАРУШЕНИЯМ СЕГМЕНТАЦИИ (я лично видел эти проблемы на машине с Linux).
Проблема с std::unique() заключается в том, что дубликаты в диапазоне [duplicationPosition,end) [включительно, эксклюзивно) в конце вектора не определены как ?.Что может случиться, так это то, что эти неопределенные ((?) элементы могут быть дополнительными или отсутствующими дубликатами.
Проблема в том, что std::unique() не предназначен для правильной обработки вектора указателей.Причина в том, что std::unique копирует уникальные значения от конца вектора «вниз» к началу вектора.Для вектора простых объектов вызывается COPY CTOR, и если COPY CTOR написан правильно, проблем с утечками памяти не возникает.Но когда это вектор указателей, не существует COPY CTOR, кроме «побитового копирования», и поэтому сам указатель просто копируется.
Есть способы решить эту утечку памяти, кроме использования интеллектуального указателя.Один из способов написать свою собственную слегка измененную версию std::unique() как «your_company::unique()».Основной трюк заключается в том, что вместо копирования элемента вы меняете местами два элемента.И вы должны быть уверены, что вместо сравнения двух указателей вы вызываете BinaryPredicate, который следует за двумя указателями на сам объект, и сравниваете содержимое этих двух производных объектов «Monster».
1) @SEE_ALSO: http://www.cplusplus.com/reference/algorithm/unique/
2) @SEE_ALSO: Какой самый эффективный способ удалить дубликаты и отсортировать вектор?
Вторая ссылка отлично написана и будет работать для std::vector, но имеет утечки памяти, освобождение дубликатов (иногда приводящее к нарушениям СЕГМЕНТАЦИИ) для std::vector
3) @SEE_ALSO:валгринд(1).ЭТОТ инструмент «утечки памяти» в LINUX удивителен тем, что он может найти!Я НАСТОЯТЕЛЬНО рекомендую использовать его!
Я надеюсь опубликовать хорошую версию «my_company::unique()» в будущем посте.На данный момент это не идеально, потому что я хочу, чтобы версия с 3 аргументами, имеющая BinaryPredicate, беспрепятственно работала как с указателем функции, так и с FUNCTOR, и у меня возникают некоторые проблемы с правильной обработкой обоих.ЕСЛИ я не смогу решить эти проблемы, я опубликую то, что у меня есть, и позволю сообществу попытаться улучшить то, что я сделал до сих пор.