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


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

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

vector<Enemy*> Enemies;

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

enemies.push_back(new Monster());

что мне нужно знать, чтобы избежать утечек памяти и других проблем?

4 63

4 ответа:

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_ptrS существует для объекта, и когда последний перестает существовать (этот счетчик переходит в ноль), он освобождает указатель. Копирование просто увеличивает количество ссылок (а перемещение передает право собственности по более низкой, почти бесплатной цене). Вы делаете их с 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. у вас есть вектор, как вектор
  2. вы нажимаете указатели на этот вектор после выделения объектов в куче
  3. вы хотите сделать push_back производного * указателя в этот вектор.

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

  1. вектор не освобождает память объекта, на который указывает указатель. Вы должны удалить его сам.
  2. ничего характерные для вектора, но деструктор базового класса должен быть виртуальным.
  3. vector и vector - это два совершенно разных типа.

проблема с использованием vector<T*> это то, что всякий раз, когда вектор неожиданно выходит из области видимости (например, когда возникает исключение), вектор очищается после себя, но это только освободит память, которую он управляет для удержания указатель, а не память, которую вы выделили для того, на что ссылаются указатели. Так что GMan это delete_pointed_to функции имеет ограниченное значение, так как он работает только тогда, когда ничего не идет не так.

что нужно сделать, это использовать смарт указатель:

vector< std::tr1::shared_ptr<Enemy> > Enemies;

(если ваш std lib поставляется без TR1, используйте .) За исключением очень редких угловых случаев (круговые ссылки) это просто устраняет проблему времени жизни объекта.

Edit: обратите внимание, что GMan в своем подробном ответе тоже упоминает об этом.

одна вещь, чтобы быть очень осторожным, если есть два монстра() производные объекты, содержимое которых идентичны по значению. Предположим, что вы хотите удалить дубликаты объектов-монстров из своего вектора (указатели базового класса на производные объекты-монстры). Если вы использовали стандартную идиому для удаления дубликатов (sort, unique, erase: см. ссылку #2], вы столкнетесь с проблемами утечки памяти и / или проблемами удаления дубликатов, что может привести к сегментации VOIOLATIONS (я лично видел эти проблемы на машине LINUX).

проблема с std:: unique() заключается в том,что дубликаты в диапазоне [duplicatePosition, end) [inclusive, exclusive) в конце вектора не определены как ?. Что может произойти, что те неопределенные ((?) элементы могут быть дополнительным дубликатом или отсутствующим дубликатом.

проблема в том, что std::unique() не приспособлен для правильной обработки вектора указателей. Причина в том, что std:: unique копирует uniques с конца вектора " вниз" к началу вектора. Для векторного простых объектов, при этом вызывается конструктор копирования, а если конструктор копирования написан правильно, нет проблемы утечки памяти. Но когда его вектор указателей, нет копии CTOR, кроме "побитовой копии", и поэтому сам указатель просто копируется.

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

1) @SEE_ALSO:http://www.cplusplus.com/reference/algorithm/unique/

2) @SEE_ALSO:каков наиболее эффективный способ удаления дубликатов и сортировать вектор?

2-я ссылка отлично написана и будет работать для std:: vector, но имеет утечки памяти, дублирование освобождает (иногда приводя к нарушениям сегментации) для std:: vector

3) @SEE_ALSO: valgrind (1). Эта" утечка памяти " инструмент на LINUX удивительно в том, что он может найти! Я очень рекомендую использовать его!

Я надеюсь опубликовать хорошую версию "my_company:: unique()" в будущем сообщении. Прямо сейчас, это не идеально, потому что я хочу 3-arg версия, имеющая BinaryPredicate, чтобы работать без проблем для указателя функции или функтора, и у меня возникли некоторые проблемы с правильной обработкой обоих. Если я не смогу решить эти проблемы, я опубликую то, что у меня есть, и пусть сообщество улучшит то, что я сделал до сих пор.