Как избежать утечек памяти при использовании вектора указателей для динамически размещаемых объектов в C ++? - PullRequest
65 голосов
/ 01 сентября 2009

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

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

vector<Enemy*> Enemies;

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

enemies.push_back(new Monster());

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

Ответы [ 4 ]

143 голосов
/ 01 сентября 2009

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

9 голосов
/ 01 сентября 2009

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

Вам нужно использовать умный указатель:

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

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

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

9 голосов
/ 01 сентября 2009

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

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

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

  1. Vector не освобождает память объекта, на который указывает указатель. Вы должны удалить его сами.
  2. Ничего специфичного для вектора, но деструктор базового класса должен быть виртуальным.
  3. vector и vector <производные *> - это два совершенно разных типа.
0 голосов
/ 09 февраля 2013

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

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

Проблема в том, что 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: Какой самый эффективный способ удаления дубликатов и сортировки вектора?

2-я ссылка отлично написана и будет работать для std :: vector, но имеет утечки памяти, дублирующие освобождения (иногда приводящие к нарушениям СЕГМЕНТАЦИИ) для std :: vector

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...