Question

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

Était-ce utile?

La solution

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

Autres conseils

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 chaque fois que vous le pouvez. Comme je l'ai déjà vu plusieurs personnes (dont le Dr Stroustrup), 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 au départ.

N'écrivez pas

Object* x = new Object;

ou même

shared_ptr<Object> x(new Object);

quand vous pouvez simplement écrire

Object x;

Utilisez RAII

  • Oubliez la récupération de place (utilisez plutôt RAII). Notez que même Garbage Collector peut également fuir (si vous oubliez de "null" certaines références en Java / C #), et que Garbage Collector ne vous aidera pas à disposer de ressources (si vous avez un objet qui a acquis un handle 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 motif "dispose" en C #).
  • Oubliez le " un retour par fonction " règle . C’est un bon conseil pour éviter les fuites, mais il est obsolète en C ++ à cause de son utilisation d’exceptions (utilisez plutôt RAII).
  • Et bien que le "motif sandwich" soit un bon conseil C, il est obsolète en C ++ en raison de son utilisation d'exceptions (utilisez plutôt la norme RAII)

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

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

RAII est la base 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 aucune).

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 Psalm33 ), RAII s’appuie sur trois concepts:

  • Une fois que l'objet est construit, cela fonctionne! Acquérir des ressources dans le constructeur.
  • La destruction d'un objet suffit! Libérez des ressources dans le destructeur.
  • Tout est une question de portées! Les objets traités (voir l’exemple de doRAIstatic ci-dessus) seront construits lors de leur déclaration et seront détruits dès que l’exécution quittera l’étendue, quelle que soit la sortie (retour, pause, 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 avec new , tous seront en quelque sorte étendus (par exemple, attachés à un pointeur intelligent).

En tant que développeur, ceci est très puissant, car vous n’aurez plus à vous soucier de la gestion manuelle des ressources (comme c’est le cas en C ou de certains objets en Java qui utilisent intensivement try / enfin pour ce cas) ...

Modifier (2012-02-12)

  

"Les objets dont la portée est définie ... seront détruits ... quelle que soit 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.

     

& # 8211; wilhelmtell

wilhelmtell a parfaitement raison à cet égard: il existe des moyens exceptionnels de tromper RAII, tous conduisant à l'arrêt brutal du processus.

Ce sont des moyens exceptionnels car le code C ++ n'est pas encombré de terminaisons, de sorties, etc., ou dans le cas d'exceptions, nous souhaitons un exception non gérée afin de bloquer le processus et de vider son image mémoire telle quelle, 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 toujours se produire.

(qui appelle terminer ou exit dans un code C ++ occasionnel? ... je me souviens avoir dû traiter ce problème lorsque vous jouiez avec GLUT : cette bibliothèque est très orientée C, allant même jusqu'à la concevoir activement pour rendre la tâche difficile au C ++. Les développeurs préfèrent ne pas se soucier de empiler des données allouées ou de " décisions intéressantes sur les ne revenant jamais de leur boucle principale ... ... Je ne ferai pas de commentaire à ce sujet) .

Vous voudrez vous pencher sur les pointeurs intelligents, tels que boost les pointeurs intelligents .

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. Donc, si vous avez plusieurs utilisateurs de votre objet, vous ne devrez pas savoir si l'objet est toujours en cours d'utilisation. Lorsque personne ne se réfère à votre 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. Bien souvent, votre "publication" " Tracez-le dans un autre thread pour que le travail soit terminé APRÈS que la création de la portée soit terminée. Ceci 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 ...

Consultez RAII et assurez-vous de bien le comprendre.

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 à une fin particulière.

Si vous avez besoin de "créer" 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 STL vectoriels et cartographiques, mais présentent quelques différences:

  • Ces collections ne peuvent pas être 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é pour tous les objets de la collection. (J'ai une autre version où il affirme être détruit et non vide.)
  • Puisqu'ils stockent des pointeurs, vous pouvez également stocker des objets hérités dans ces conteneurs.

Mon type 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 une utilisation dans ces conteneurs.

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

Règles très strictes en matière de "propriété" - quel objet ou partie du logiciel a le droit de supprimer l'objet. Effacez les commentaires et les noms de variables judicieux pour indiquer clairement si un pointeur "possède" " ou est "regardez, ne touchez pas". Pour vous aider à décider qui possède quoi, suivez autant que possible le "sandwich" motif dans chaque sous-routine 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 "propriétaire". pointeurs. 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 "à la recherche". 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 ne n'appelle pas la destruction de 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 élevé. Peut-être que les nouvelles voies ont un avantage là-bas!

Excellente 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 des travaux intéressants d'auteurs divers, je peux vous donner un indice:

  • L'allocateur de taille fixe fait l'objet de nombreuses discussions, partout dans le réseau

  • L’allocation de petits objets a été introduite par Alexandrescu en 2001 dans son livre parfait "Modern c ++ design"

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

  • Vous trouverez une excellente liste de ressources dans cet article

Ne commencez pas à écrire vous-même un allocateur inutile pour Noob ... DOCUMENTZ-VOUS d’abord.

La RAII est une technique couramment utilisée avec la gestion de la mémoire en C ++. En gros, vous utilisez des constructeurs / destructeurs pour gérer l’allocation de ressources. Bien sûr, il existe d’autres détails désagréables en C ++ dus à la sécurité des exceptions, mais l’idée de base est assez simple.

Le problème est généralement lié à la propriété. Je recommande fortement de lire les séries Effective C ++ de Scott Meyers et Modern C ++ Design d’Andrei Alexandrescu.

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 à:

Utilisez des pointeurs intelligents partout où vous le pouvez! Des classes entières de fuites de mémoire disparaissent tout simplement.

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.)

valgrind est également 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 prendre 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.

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, si vous ne respectez pas cette règle, que vous avez un destructeur virtuel.

Si vous ne pouvez pas / n'utilisez pas de pointeur intelligent pour quelque chose (même si cela doit ê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 de saisir le code de la portée

.

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 devient propriétaire 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 son 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. La même fonction aurait pu accepter un pointeur non-const, et l'appelant aurait peut-être supposé que l'appelé avait accepté la propriété, mais avec une référence const, il n'y avait aucune question.

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

Je ne suis pas d’accord avec les commentaires qui recommandent de compter les 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.

Astuces par ordre d'importance:

- Conseil n ° 1 N'oubliez jamais de déclarer vos destructeurs "virtuel".

- Conseil n ° 2: utilisez RAII

- Conseil n ° 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 dû déboguer mes propres pointeurs intelligents. Je ne prendrai certainement plus le même chemin, mais encore une fois, je ne peux pas ajouter de boost à nos dépendances)

- Conseil n ° 5 Si cela fonctionne de manière occasionnelle / non critique (comme dans les jeux avec des milliers d'objets), regardez le conteneur de pointeur de boost de Thorsten Ottosen

- Conseil n ° 6 Recherchez un en-tête de détection de fuite pour la plate-forme de votre choix, tel que la détection visuelle des fuites " vld & en-tête

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 sa responsabilité.

Ces sémantiques s’appliquent également aux paramètres. Si l'appelant vous transmet un auto_ptr, il vous en donne la propriété.

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 localiser les problèmes de mémoire une fois que vous les avez.

Valgrind memcheck est un excellent logiciel gratuit.

Pour MSVC uniquement, ajoutez les éléments suivants 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, toutes les fuites qui s’ensuivront à la sortie de votre programme vous seront signalées (le suivi (nouveau / supprimé)). C’est simple, mais cela m’a aidé dans le passé.

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

Si vous souhaitez 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, je ne devrais donc pas la libérer.

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

Tout est question de propriété du pointeur.

  • 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 et la désallocation dynamiques, 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 écrivains ;-).
  • 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. Ainsi, les objets ne peuvent pas être libérés "accidentellement". (sauf si vous jetez le constent ;-))).
  • Réduisez au minimum le nombre d'emplacements dans le 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 ;-)).
    De cette façon, vous pouvez créer facilement une sortie de débogage (quelles adresses sont allouées et désallouée, ...), 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 $ / & # 8364; / ...).

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 pour toutes les applications).

.

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

Par exemple, vérifiez dans ce site [Débogage 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 lorsque 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 le bloc de mémoire alloué qui n'a pas été supprimé, en l'identifiant par son nom de fichier et son numéro de ligne, ce qui est, je suppose, ce que vous voulez.

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

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 auriez un appel à " 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 votre sentinelle. Bien sûr, n'oubliez pas de commenter ceci pour les builds sans débogage! Cela nécessite un peu plus de mémoire, 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 attribué à 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 grands joueurs - un regroupement de certaines descriptions dans un MemDump vous indique par exemple lorsque le son prend beaucoup plus de place que prévu.

C ++ a été conçu pour RAII. 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 existe 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é.

La création de threads (paramètre que vous transmettez) est l’un des exemples concernant l’allocation et la destruction à différents endroits. 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 de fil

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? Cordialement,

Ema!

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 ...). GC ne vous aiderait pas non plus.

Exactement un retour de n'importe quelle fonction. De cette façon, vous pouvez faire la désallocation là-bas et ne jamais la manquer.

Il est trop facile de faire une erreur sinon:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top