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

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

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

vector<Enemy*> Enemies;

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

enemies.push_back(new Monster());

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

Ответ 1

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 отсутствует в стандарте С++ 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> имел очевидное применение в С++ 03, я не могу сейчас говорить об актуальности, потому что мы можем использовать std::vector<std::unique_ptr<T>>, возможно, с небольшими сопоставимыми накладными расходами, но это требование должно быть проверено.

Независимо от того, никогда явно не освобождайте вещи в своем коде. Оберните вещи, чтобы убедиться, что управление ресурсами обрабатывается автоматически. В вашем коде не должно быть никаких указателей владения.

Как по умолчанию в игре, я бы, вероятно, пошел с std::vector<std::shared_ptr<T>>. Мы ожидаем совместного использования в любом случае, это достаточно быстро, пока профилирование не говорит иначе, безопасно и легко использовать.

Ответ 2

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

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

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

  • Вектор не освобождает память объекта, на который указывает указатель. Вы должны удалить его самостоятельно.
  • Ничего конкретного для вектора, но деструктор базового класса должен быть виртуальным.
  • вектор < base * > и vector < производные * > - два совершенно разных типа.

Ответ 3

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

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

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

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

Изменить: Обратите внимание, что GMan в своем подробном ответе также упоминает об этом.

Ответ 4

Одно очень важно, если есть два объекта Monster() DERIVED, содержимое которых идентично по значению. Предположим, что вы хотели удалить объекты Monster DUPLICATE из вашего вектора (указатели класса BASE в объекты DERIVED Monster). Если вы использовали стандартную идиому для удаления дубликатов (сортировка, уникальность, стирание: см. LINK # 2), вы столкнетесь с проблемами утечки памяти и/или дублирующими проблемами удаления, что может привести к ВОССТАНОВЛЕНИЯ СЕГМЕНТАЦИИ (я лично видел эти проблемы на LINUX).

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

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

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

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

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

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

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

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