web-dev-qa-db-fra.com

Comment éviter les fuites de mémoire lors de l'utilisation d'un vecteur de pointeurs vers des objets alloués dynamiquement en C ++?

J'utilise un vecteur de pointeurs vers des objets. Ces objets sont dérivés d'une classe de base et sont alloués et stockés dynamiquement.

Par exemple, j'ai quelque chose comme:

vector<Enemy*> Enemies;

et je vais dériver de la classe Enemy puis allouer dynamiquement de la mémoire pour la classe dérivée, comme ceci:

enemies.Push_back(new Monster());

Quelles sont les choses dont je dois être conscient pour éviter les fuites de mémoire et autres problèmes?

66
akif

std::vector gérera la mémoire pour vous, comme toujours, mais cette mémoire sera constituée de pointeurs, pas d'objets.

Cela signifie que vos classes seront perdues en mémoire une fois que votre vecteur sera hors de portée. Par exemple:

#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();
}

Ce que vous devez faire est de vous assurer de supprimer tous les objets avant que le vecteur ne soit hors de portée:

#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();
}

C'est difficile à maintenir, cependant, car nous devons nous rappeler d'effectuer une action. Plus important encore, si une exception devait se produire entre l'allocation d'éléments et la boucle de désallocation, la boucle de désallocation ne s'exécuterait jamais et vous êtes de toute façon coincé avec la fuite de mémoire! C'est ce qu'on appelle la sécurité d'exception et c'est une raison essentielle pour laquelle la désallocation doit être effectuée automatiquement.

Ce serait mieux si les pointeurs se supprimaient. Ces thèses sont appelées pointeurs intelligents et la bibliothèque standard fournit std::unique_ptr et std::shared_ptr .

std::unique_ptr représente un pointeur unique (non partagé, propriétaire unique) vers une ressource. Cela devrait être votre pointeur intelligent par défaut et le remplacement complet de toute utilisation de pointeur brut.

auto myresource = /*std::*/make_unique<derived>(); // won't leak, frees itself

std::make_unique est absent de la norme C++ 11 par inadvertance, mais vous pouvez en créer vous-même. Pour créer directement un unique_ptr (déconseillé sur make_unique si vous le pouvez), procédez comme suit:

std::unique_ptr<derived> myresource(new derived());

Les pointeurs uniques ont uniquement une sémantique de déplacement; ils ne peuvent pas être copiés:

auto x = myresource; // error, cannot copy
auto y = std::move(myresource); // okay, now myresource is empty

Et c'est tout ce dont nous avons besoin pour l'utiliser dans un conteneur:

#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 a une sémantique de copie de comptage de références; il permet à plusieurs propriétaires de partager l'objet. Il suit le nombre de shared_ptrs existent pour un objet, et lorsque le dernier cesse d'exister (ce nombre passe à zéro), il libère le pointeur. La copie augmente simplement le nombre de références (et le transfert de propriété des transferts à un coût inférieur, presque gratuit). Vous les faites avec std::make_shared (ou directement comme indiqué ci-dessus, mais parce que shared_ptr doit faire des allocations en interne, il est généralement plus efficace et techniquement plus sûr d'exception à utiliser 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();
}

N'oubliez pas que vous souhaitez généralement utiliser std::unique_ptr par défaut car il est plus léger. Aditionellement, std::shared_ptr peut être construit à partir d'un std::unique_ptr (mais pas l'inverse), il est donc normal de commencer petit.

Vous pouvez également utiliser un conteneur créé pour stocker des pointeurs vers des objets, tels qu'un 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();
}

Tandis que boost::ptr_vector<T> avait une utilisation évidente en C++ 03, je ne peux pas parler de la pertinence maintenant parce que nous pouvons utiliser std::vector<std::unique_ptr<T>> avec probablement peu ou pas de frais généraux comparables, mais cette affirmation doit être testée.

Quoi qu'il en soit, ne libérez jamais explicitement des choses dans votre code . Récapitulez les choses pour vous assurer que la gestion des ressources est traitée automatiquement. Vous ne devez avoir aucun pointeur propriétaire brut dans votre code.

Par défaut dans un jeu, j'irais probablement avec std::vector<std::shared_ptr<T>>. Nous attendons le partage de toute façon, c'est assez rapide jusqu'à ce que le profilage dise le contraire, c'est sûr et c'est facile à utiliser.

145
GManNickG

Je suppose ce qui suit:

  1. Vous avez un vecteur comme le vecteur <base *>
  2. Vous poussez les pointeurs vers ce vecteur après avoir alloué les objets sur le tas
  3. Vous voulez faire un Push_back de pointeur dérivé * dans ce vecteur.

Les choses suivantes me viennent à l'esprit:

  1. Le vecteur ne libérera pas la mémoire de l'objet pointé par le pointeur. Vous devez le supprimer lui-même.
  2. Rien de spécifique au vecteur, mais le destructeur de classe de base doit être virtuel.
  3. le vecteur <base *> et le vecteur <dérivé *> sont deux types totalement différents.
9
Naveen

Le problème avec l'utilisation de vector<T*> est que, chaque fois que le vecteur sort de la portée de manière inattendue (comme lorsqu'une exception est levée), le vecteur se nettoie après vous-même, mais cela ne libérera que la mémoire qu'il gère pour contenir le pointeur , pas la mémoire que vous avez allouée pour ce à quoi les pointeurs font référence. Alors GMan's delete_pointed_to function a une valeur limitée, car elle ne fonctionne que lorsque rien ne va mal.

Ce que vous devez faire est d'utiliser un pointeur intelligent:

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

(Si votre bibliothèque std est livrée sans TR1, utilisez boost::shared_ptr à la place.) À l'exception de très rares cas d'angle (références circulaires), cela élimine simplement les problèmes de durée de vie des objets.

Edit : Notez que GMan, dans sa réponse détaillée, le mentionne également.

9
sbi