Как избежать утечек памяти при использовании вектора указателей на динамически выделяемые объекты в C ++?

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

Вопрос

Я использую вектор указателей на объекты.Эти объекты являются производными от базового класса и динамически распределяются и хранятся.

Например, у меня есть что-то вроде:

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>>.В любом случае мы ожидаем обмена, это происходит достаточно быстро, пока профилирование не скажет иное, это безопасно и легко в использовании.

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

Я предполагаю следующее:

  1. У вас есть вектор типа вектор< base* >
  2. Вы помещаете указатели на этот вектор после размещения объектов в куче.
  3. Вы хотите выполнить push_back указателя производного* в этот вектор.

Мне на ум приходят следующие вещи:

  1. Вектор не освобождает память объекта, на который указывает указатель.Вам придется удалить его самому.
  2. Ничего особенного для вектора, но деструктор базового класса должен быть виртуальным.
  3. вектор< 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, и у меня возникают некоторые проблемы с правильной обработкой обоих.ЕСЛИ я не смогу решить эти проблемы, я опубликую то, что у меня есть, и позволю сообществу попытаться улучшить то, что я сделал до сих пор.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top