web-dev-qa-db-fra.com

Instructions générales pour éviter les fuites de mémoire en C ++

Quels sont quelques conseils généraux pour vous assurer de ne pas perdre de mémoire dans les programmes C++? Comment savoir qui doit libérer la mémoire allouée dynamiquement?

125
dulipishi

Au lieu de gérer la mémoire manuellement, essayez d'utiliser des pointeurs intelligents, le cas échéant.
Regardez les Boost lib , TR1 , et pointeurs intelligents .
De plus, les pointeurs intelligents font maintenant partie de la norme C++ appelée C++ 11 .

38
Andri Möll

J'approuve totalement tous les conseils concernant RAII et les pointeurs intelligents, mais j'aimerais également ajouter un conseil de niveau légèrement supérieur: la mémoire la plus facile à gérer est la mémoire que vous n'avez jamais allouée. Contrairement aux langages tels que C # et Java, où presque tout est une référence, en C++, vous devez placer des objets sur la pile dès que vous le pouvez. Comme plusieurs personnes (y compris le Dr Stroustrup) l'ont souligné, la principale raison pour laquelle la récupération de place n'a jamais été aussi populaire en C++ est que le C++ bien écrit ne produit pas beaucoup de déchets dès le départ.

Ne pas écrire

Object* x = new Object;

ou même

shared_ptr<Object> x(new Object);

quand tu peux juste écrire

Object x;
196
Ross Smith

Utilisez RAII

  • Oubliez la récupération de place (utilisez plutôt RAII). Notez que même le récupérateur de place peut également fuir (si vous oubliez de "supprimer" certaines références en Java/C #), et que Garbage Collector ne vous aidera pas à disposer des ressources (si vous avez un objet qui a acquis un Dans un fichier, le fichier ne sera pas libéré automatiquement lorsque l'objet sortira de sa portée si vous ne le faites pas manuellement en Java, ou utilisez le modèle "dispose" en C #).
  • Oubliez la règle "un retour par fonction" . C est un bon conseil pour éviter les fuites, mais il est obsolète en C++ en raison de son utilisation des exceptions (utilisez plutôt RAII).
  • Et bien que le "motif sandwich" soit un bon conseil en C, il est obsolète en C++ en raison de son utilisation des exceptions (utilisez plutôt RAII).

Ce message semble être répétitif, mais en C++, le modèle le plus fondamental à connaître est RAII .

Apprenez à utiliser les pointeurs intelligents, à la fois boost, TR1 ou même auto_ptr (bien que souvent assez efficace) (mais vous devez connaître ses limites).

RAII est la base à la fois de la sécurité des exceptions et de la suppression des ressources en C++, et aucun autre modèle (sandwich, etc.) ne vous donnera les deux (et la plupart du temps, il ne vous en fournira aucun).

Voir ci-dessous une comparaison des codes RAII et non RAII:

void doSandwich()
{
   T * p = new T() ;
   // do something with p
   delete p ; // leak if the p processing throws or return
}

void doRAIIDynamic()
{
   std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

void doRAIIStatic()
{
   T p ;
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

À propos de RAII

Pour résumer (après le commentaire de Ogre Psaume 33), RAII repose sur trois concepts:

  • Une fois que l'objet est construit, ça fonctionne! Acquérir des ressources dans le constructeur.
  • La destruction d'objet suffit! Faites des ressources gratuites dans le destructeur.
  • Tout est question de portée! Les objets scopés (voir l'exemple doRAIIStatic ci-dessus) seront construits lors de leur déclaration et seront détruits au moment où l'exécution quittera le scope, quelle que soit la sortie (return, break, exception, etc.).

Cela signifie que dans le code C++ correct, la plupart des objets ne seront pas construits avec new et seront déclarés sur la pile. Et pour ceux construits en utilisant new, tout sera en quelque sorte portée (par exemple, attaché à un pointeur intelligent).

En tant que développeur, cela est très puissant, car vous n’aurez plus à vous soucier de la gestion manuelle des ressources (comme cela se fait en C ou pour certains objets dans Java qui utilise intensivement try/finally pour ce cas) ...

Modifier (2012-02-12)

"les objets délimités ... seront détruits ... peu importe la sortie", ce n'est pas tout à fait vrai. il existe des moyens de tricher RAII. toute saveur de terminate () ignorera le nettoyage. exit (EXIT_SUCCESS) est un oxymore à cet égard.

- wilhelmtell

wilhelmtell a tout à fait raison à ce sujet: il existe exceptionnel des moyens de tromper RAII, qui conduisent tous à un arrêt brutal du processus.

Celles-ci sont exceptionnelles car le code C++ n’est pas encombré de terminaisons, de sorties, etc., ou dans le cas d’exceptions, nous voulons un exception non gérée pour mettre le processus en panne et le core vider son image mémoire en l'état, et non après le nettoyage.

Mais nous devons toujours être au courant de ces cas car, même s'ils se produisent rarement, ils peuvent quand même arriver.

(qui appelle terminate ou exit dans du code C++ occasionnel? ... je me souviens avoir dû traiter ce problème lorsque je jouais avec SATURÉ : Cette bibliothèque est très orientée C, allant même jusqu'à la concevoir activement pour rendre les choses difficiles pour les développeurs C++, comme ne pas se soucier de empiler les données allouées , ou avoir des décisions "intéressantes" sur - ne revenant jamais de leur boucle principale ... je ne ferai pas de commentaire à ce sujet).

101
paercebal

Vous voudrez regarder les pointeurs intelligents, tels que les pointeurs intelligents de boost .

Au lieu de

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

boost :: shared_ptr sera automatiquement supprimé une fois le nombre de références égal à zéro:

int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

Notez ma dernière note, "lorsque le nombre de références est égal à zéro, ce qui est la partie la plus cool. Ainsi, si vous avez plusieurs utilisateurs de votre objet, vous ne devez pas savoir si cet objet est toujours en cours d'utilisation. pointeur partagé, il est détruit.

Ce n'est cependant pas une panacée. Bien que vous puissiez accéder au pointeur de base, vous ne voudriez pas le passer à une API tierce si vous n’êtes pas sûr de ce qu’il fait. Beaucoup de fois, votre "publication" trucs à un autre thread pour que le travail soit fait APRÈS que la création soit terminée. C'est commun avec PostThreadMessage dans Win32:

void foo()
{
   boost::shared_ptr<Object> obj(new Object()); 

   // Simplified here
   PostThreadMessage(...., (LPARAM)ob.get());
   // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}

Comme toujours, utilisez votre casse-tête avec n'importe quel outil ...

25
Doug T.

Lisez sur RAII et assurez-vous de bien le comprendre.

12
Hank

La plupart des fuites de mémoire résultent de l'incertitude quant à la propriété et à la durée de vie des objets.

La première chose à faire est d’allouer sur la pile chaque fois que vous le pouvez. Cela concerne la plupart des cas où vous devez attribuer un seul objet à certaines fins.

Si vous avez besoin de "renouveler" un objet, il aura la plupart du temps un seul propriétaire évident pour le reste de sa vie. Pour cette situation, j’ai tendance à utiliser un ensemble de modèles de collections conçus pour "posséder" des objets qui y sont stockés par un pointeur. Ils sont implémentés avec les conteneurs de vecteur et de carte STL mais présentent quelques différences:

  • Ces collections ne peuvent être ni copiées ni attribuées. (une fois qu'ils contiennent des objets.)
  • Les pointeurs sur les objets y sont insérés.
  • Lorsque la collection est supprimée, le destructeur est d'abord appelé sur tous les objets de la collection. (J'ai une autre version où il affirme être détruit et non vide.)
  • Comme ils stockent des pointeurs, vous pouvez également stocker des objets hérités dans ces conteneurs.

Mon problème avec STL est qu’il est tellement concentré sur les objets Value alors que dans la plupart des applications, les objets sont des entités uniques pour lesquelles aucune sémantique de copie significative n’est requise pour utilisation dans ces conteneurs.

11
Jeroen Dirks

Bah, vous, jeunes enfants, et vos éboueurs à la mode ...

Règles très strictes sur la "propriété" - quel objet ou partie du logiciel a le droit de supprimer l'objet. Effacer les commentaires et les noms de variables judicieux pour le rendre évident si un pointeur "possède" ou est "il suffit de regarder, ne touchez pas". Pour vous aider à décider qui possède quoi, suivez autant que possible le motif "sandwich" dans chaque sous-programme ou méthode.

create a thing
use that thing
destroy that thing

Parfois, il est nécessaire de créer et de détruire dans des endroits très différents. Je pense qu'il est difficile d'éviter cela.

Dans tout programme nécessitant des structures de données complexes, je crée un arbre strict contenant des objets contenant d’autres objets - en utilisant des pointeurs "propriétaires". Cette arborescence modélise la hiérarchie de base des concepts de domaine d'application. Exemple, une scène 3D possède des objets, des lumières, des textures. À la fin du rendu, lorsque le programme se ferme, il existe un moyen clair de tout détruire.

De nombreux autres pointeurs sont définis selon les besoins chaque fois qu'une entité a besoin d'accéder à une autre, pour parcourir des tableaux ou autre chose; ce sont les "juste regarder". Pour l'exemple de la scène 3D - un objet utilise une texture mais ne possède pas; d'autres objets peuvent utiliser cette même texture. La destruction d'un objet not appelle la destruction de toutes les textures.

Oui, cela prend du temps, mais c'est ce que je fais. J'ai rarement des fuites de mémoire ou d'autres problèmes. Mais ensuite, je travaille dans le domaine limité des logiciels de hautes performances scientifiques, d'acquisition de données et graphiques. Je ne traite pas souvent des transactions comme dans le secteur bancaire et du commerce électronique, des interfaces utilisateur graphiques événementielles ou un chaos asynchrone en réseau. Peut-être que les nouvelles voies ont un avantage là-bas!

10
DarenW

Bonne question!

si vous utilisez c ++ et que vous développez une application boud en temps réel avec processeur et mémoire (comme des jeux), vous devez écrire votre propre gestionnaire de mémoire.

Je pense que mieux vous pouvez faire est de fusionner quelques travaux intéressants d'auteurs différents, je peux vous donner un indice:

  • L'allocateur de taille fixe est très discuté, partout dans le net

  • Small Object Allocation a été introduit par Alexandrescu en 2001 dans son livre parfait "Modern c ++ design"

  • Un grand progrès (avec le code source distribué) peut être trouvé dans un article étonnant de Game Programming Gem 7 (2008) intitulé "High Performance Heap allocator" écrit par Dimitar Lazarov.

  • Une grande liste de ressources peut être trouvée dans this article

Ne commencez pas à écrire vous-même un allocateur inutile de noob ... DOCUMENTZ-VOUS d'abord.

8
ugasoft

Une technique qui est devenue populaire avec la gestion de la mémoire en C++ est RAII . En gros, vous utilisez des constructeurs/destructeurs pour gérer l’allocation de ressources. Bien sûr, il y a d'autres détails désagréables en C++ en raison de la sécurité des exceptions, mais l'idée de base est assez simple.

Le problème se résume généralement à celui de la propriété. Je recommande fortement de lire les séries Effective C++ de Scott Meyers et Modern C++ Design de Andrei Alexandrescu.

5
Jason Dagit

Il y a déjà beaucoup de choses sur la façon de ne pas fuir, mais si vous avez besoin d'un outil pour vous aider à suivre les fuites, jetez un coup d'œil à:

5
fabiopedrosa

Les utilisateurs astucieux partout où vous le pouvez! Des classes entières de fuites de mémoire disparaissent tout simplement.

4
DougN

Partagez et connaissez les règles de propriété de la mémoire dans votre projet. L'utilisation des règles COM permet une meilleure cohérence (les paramètres [in] appartiennent à l'appelant, l'appelé doit copier; les paramètres [out] appartiennent à l'appelant, l'appelant doit en faire une copie s'il conserve une référence, etc.).

4
Seth Morris

valgrind est un bon outil pour vérifier les fuites de mémoire de vos programmes au moment de l'exécution.

Il est disponible sur la plupart des versions de Linux (y compris Android) et sur Darwin.

Si vous avez l'habitude d'écrire des tests unitaires pour vos programmes, vous devriez avoir l'habitude de lancer systématiquement valgrind sur les tests. Cela évitera potentiellement de nombreuses fuites de mémoire à un stade précoce. Il est également généralement plus facile de les localiser lors de tests simples que dans un logiciel complet.

Bien entendu, ces conseils restent valables pour tout autre outil de vérification de la mémoire.

4
Joseph

De même, n'utilisez pas de mémoire allouée manuellement s'il existe une classe de bibliothèque std (par exemple, un vecteur). Assurez-vous que si vous ne respectez pas cette règle, vous disposez d'un destructeur virtuel.

3
Joseph

Si vous ne pouvez pas/n'utilisez pas de pointeur intelligent pour quelque chose (même si cela devrait être un énorme drapeau rouge), tapez votre code avec:

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

C'est évident, mais assurez-vous de le taper avant vous tapez n'importe quel code dans la portée

2
Seth Morris

Astuces par ordre d'importance:

-Tip # 1 Rappelez-vous toujours de déclarer vos destructeurs "virtuels".

-Tip # 2 Utilisez RAII

-Tip # 3 Utilisez les smartpointers de boost

- Astuce n ° 4 N'écrivez pas vos propres Smartpointers buggy, utilisez boost (sur un projet sur lequel je suis en ce moment, je ne peux pas utiliser boost et j'ai du mal à déboguer mes propres pointeurs intelligents, je ne prendrais certainement pas le même itinéraire encore, mais encore une fois maintenant je ne peux pas ajouter de boost à nos dépendances)

Astuce # 5 Si cela fonctionne occasionnellement/non-critique (comme dans les jeux avec des milliers d'objets), regardez le conteneur de pointeur de boost de Thorsten Ottosen

-Tip # 6 Trouvez un en-tête de détection de fuite pour la plate-forme de votre choix, tel que l'en-tête "vld" de Visual Leak Detection

2
Robert Gould

Une source fréquente de ces bogues est quand vous avez une méthode qui accepte une référence ou un pointeur sur un objet mais laisse la propriété incertaine. Les conventions de style et de commentaire peuvent rendre cela moins probable.

Soit le cas particulier où la fonction prend possession de l'objet. Dans toutes les situations où cela se produit, veillez à écrire un commentaire à côté de la fonction dans le fichier d'en-tête l'indiquant. Vous devez vous efforcer de vous assurer que, dans la plupart des cas, le module ou la classe qui alloue un objet est également responsable de sa désallocation.

L'utilisation de const peut être très utile dans certains cas. Si une fonction ne modifie pas un objet et ne stocke pas une référence qui persiste après son retour, acceptez une référence const. À la lecture du code de l'appelant, il sera évident que votre fonction n'a pas accepté la propriété de l'objet. Vous auriez pu demander à la même fonction d’accepter un pointeur non-const, et l’appelant a pu ou non supposer que l’appelé avait accepté la propriété, mais avec une référence const, la question ne se posait pas.

N'utilisez pas de références non const dans les listes d'arguments. Lors de la lecture du code de l'appelant, il est très difficile de savoir que l'appelé a peut-être conservé une référence au paramètre.

Je suis en désaccord avec les commentaires recommandant des pointeurs comptés. Cela fonctionne généralement bien, mais quand vous avez un bogue et que cela ne fonctionne pas, surtout si votre destructeur fait quelque chose de non trivial, comme dans un programme multithread. Essayez définitivement d’ajuster votre conception de manière à ne pas avoir besoin de compter les références si ce n’est pas trop difficile.

2
Jonathan

Si vous le pouvez, utilisez boost shared_ptr et auto_ptr C++ standard. Ceux-ci véhiculent une sémantique de propriété.

Lorsque vous renvoyez un auto_ptr, vous dites à l'appelant que vous lui conférez la propriété de la mémoire.

Lorsque vous renvoyez un shared_ptr, vous dites à l'appelant que vous avez une référence à celui-ci et qu'il en fait partie, mais ce n'est pas uniquement sous sa responsabilité.

Cette sémantique s'applique également aux paramètres. Si l'appelant vous transmet un auto_ptr, il vous en donne la propriété.

1
Justin Rudd
  • Essayez d'éviter d'allouer des objets de manière dynamique. Tant que les classes ont des constructeurs et des destructeurs appropriés, utilisez une variable du type de classe, et non un pointeur sur celle-ci, et vous éviterez l'allocation dynamique et la désallocation car le compilateur le fera pour vous.
    En fait, c’est aussi le mécanisme utilisé par les "pointeurs intelligents" et appelé RAII par certains des autres auteurs ;-).
  • Lorsque vous transmettez des objets à d'autres fonctions, préférez les paramètres de référence aux pointeurs. Cela évite certaines erreurs possibles.
  • Déclarez les paramètres const, si possible, en particulier les pointeurs sur les objets. De cette façon, les objets ne peuvent pas être libérés "accidentellement" (sauf si vous jetez le const de suite ;-))).
  • Réduisez au minimum le nombre d'emplacements du programme où vous effectuez l'allocation de mémoire et la désallocation. Par exemple. si vous allouez ou libérez le même type plusieurs fois, écrivez une fonction pour celui-ci (ou une méthode fabrique ;-)).
    Ainsi, vous pouvez créer facilement une sortie de débogage (adresses attribuées et désallouées, ...), si nécessaire.
  • Utilisez une fonction fabrique pour allouer des objets de plusieurs classes liées à partir d'une seule fonction.
  • Si vos classes ont une classe de base commune avec un destructeur virtuel, vous pouvez toutes les libérer en utilisant la même fonction (ou méthode statique).
  • Vérifiez votre programme avec des outils tels que purifier (malheureusement beaucoup de $/€/...).
1
mh.

Si vous allez gérer votre mémoire manuellement, vous avez deux cas:

  1. J'ai créé l'objet (peut-être indirectement, en appelant une fonction qui alloue un nouvel objet), je l'utilise (ou une fonction que j'appelle l'utilise), puis je le libère.
  2. Quelqu'un m'a donné la référence, alors je ne devrais pas la libérer.

Si vous devez enfreindre l'une de ces règles, veuillez la documenter.

Tout est question de propriété du pointeur.

1
Null303

valgrind (seulement disponible pour * nix plates-formes) est un vérificateur de mémoire très agréable

1
Ronny Brendel

D'autres ont mentionné des moyens d'éviter les fuites de mémoire (comme des pointeurs intelligents). Mais un outil de profilage et d'analyse de la mémoire est souvent le seul moyen de dépister les problèmes de mémoire une fois que vous les avez.

Valgrind memcheck est un excellent libre.

1
eli

Pour MSVC uniquement, ajoutez ce qui suit en haut de chaque fichier .cpp:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Ensuite, lors du débogage avec VS2003 ou une version ultérieure, vous serez informé de toute fuite lors de la fermeture du programme (le suivi/l’effacement est nouveau). C'est basique, mais cela m'a aidé par le passé.

1
Rob

C++ est conçu RAII à l'esprit. Je pense qu'il n'y a vraiment pas de meilleur moyen de gérer la mémoire en C++. Mais veillez à ne pas allouer de très gros morceaux (tels que des objets tampons) sur une étendue locale. Cela peut provoquer des débordements de pile et, s'il y a une faille dans la vérification des limites lors de l'utilisation de ce bloc, vous pouvez écraser d'autres variables ou renvoyer des adresses, ce qui entraîne toutes sortes de failles de sécurité.

0
artificialidiot

L'un des seuls exemples concernant l'allocation et la destruction à différents endroits est la création de threads (paramètre que vous transmettez). Mais même dans ce cas, c'est facile. Voici la fonction/méthode créant un thread:

struct myparams {
int x;
std::vector<double> z;
}

std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...

Ici à la place la fonction thread

extern "C" void* th_func(void* p) {
   try {
       std::auto_ptr<myparams> param((myparams*)p);
       ...
   } catch(...) {
   }
   return 0;
}

Pretty Easyn n'est-ce pas? Si la création du fil échoue, la ressource sera libre (supprimée) par auto_ptr, sinon la propriété sera transmise au fil. Et si le fil est si rapide qu’après sa création, il libère la ressource avant la

param.release();

se fait appeler dans la fonction/méthode principale? Rien! Parce que nous "dirons" à auto_ptr d’ignorer la désallocation. La gestion de la mémoire C++ est-elle facile, n'est-ce pas? À votre santé,

Ema!

0
Emanuele Oriani

Gérez la mémoire de la même manière que vous gérez les autres ressources (descripteurs, fichiers, connexions à la base de données, sockets, etc.). GC ne vous aiderait pas non plus.

0
Nemanja Trifunovic

Vous pouvez intercepter les fonctions d'allocation de mémoire et voir s'il existe des zones de mémoire non libérées à la sortie du programme (bien que cela ne convienne pas à toutes des applications).

Cela peut également être fait au moment de la compilation en remplaçant les opérateurs new et delete et d'autres fonctions d'allocation de mémoire.

Par exemple, vérifiez ceci site [Débogage de l'allocation de mémoire en C++] Remarque: Il existe une astuce pour l'opérateur de suppression qui ressemble à ceci:

#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE

Vous pouvez stocker dans certaines variables le nom du fichier et quand l'opérateur de suppression surchargé saura à quel endroit il a été appelé. De cette façon, vous pouvez avoir la trace de chaque suppression et malloc de votre programme. À la fin de la séquence de vérification de la mémoire, vous devriez être en mesure d'indiquer quel bloc de mémoire alloué n'a pas été supprimé, en l'identifiant par son nom de fichier et son numéro de ligne, ce qui correspond, je suppose, à ce que vous voulez.

Vous pouvez également essayer quelque chose comme BoundsChecker sous Visual Studio, ce qui est très intéressant et facile à utiliser.

0
INS

Nous encapsulons toutes nos fonctions d’allocation avec une couche qui ajoute une brève chaîne à l’avant et un drapeau sentinelle à la fin. Ainsi, par exemple, vous devriez appeler "myalloc (pszSomeString, iSize, iAlignment); ou new (" description ", iSize) MyObject (); qui alloue en interne la taille spécifiée plus suffisamment d'espace pour votre en-tête et sentinel. Bien sûr N'oubliez pas de commenter ceci pour les versions sans débogage! Cela prend un peu plus de mémoire pour le faire, mais les avantages dépassent de loin les coûts.

Cela présente trois avantages: premièrement, il vous permet de suivre facilement et rapidement le code qui fuit, en effectuant des recherches rapides pour le code alloué dans certaines "zones", mais non nettoyé lorsque ces zones auraient dû être libérées. Il peut également être utile de détecter le moment où une limite a été écrasée en vérifiant que toutes les sentinelles sont intactes. Cela nous a permis de sauver de nombreuses fois lorsque nous essayons de trouver ces plantages bien cachés ou ces erreurs de tableau. Le troisième avantage est de suivre l'utilisation de la mémoire pour voir qui sont les gros joueurs - un assemblage de certaines descriptions dans un MemDump vous indique par exemple lorsque le "son" prend beaucoup plus de place que prévu.

0
screenglow