Quels sont quelques conseils généraux pour m'assurer de ne pas fuir de mémoire dans les programmes C ++? Comment déterminer qui doit libérer de la mémoire allouée dynamiquement?
Quels sont quelques conseils généraux pour m'assurer de ne pas fuir de mémoire dans les programmes C ++? Comment déterminer qui doit libérer de la mémoire allouée dynamiquement?
Réponses:
Au lieu de gérer la mémoire manuellement, essayez d'utiliser des pointeurs intelligents le cas échéant.
Jetez un œil à Boost lib , TR1 et les pointeurs intelligents .
De plus, les pointeurs intelligents font désormais partie de la norme C ++ appelée C ++ 11 .
J'approuve entièrement tous les conseils sur 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 comme C # et Java, où à peu près tout est une référence, en C ++, vous devez mettre des objets sur la pile chaque fois que vous le pouvez. Comme je l'ai vu plusieurs personnes (y compris le Dr Stroustrup) le faire remarquer, la raison principale pour laquelle le garbage collection n'a jamais été populaire en C ++ est que C ++ bien écrit ne produit pas beaucoup de déchets en premier lieu.
N'écris pas
Object* x = new Object;
ou même
shared_ptr<Object> x(new Object);
quand tu peux juste écrire
Object x;
Ce message semble être répétitif, mais en C ++, le modèle le plus basique à connaître est RAII .
Apprenez à utiliser des pointeurs intelligents, à la fois de boost, TR1 ou même du modeste (mais souvent assez efficace) auto_ptr (mais vous devez connaître ses limites).
RAII est à la fois la base de la sécurité des exceptions et de l'élimination 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 donnera aucun).
Voir ci-dessous une comparaison du code 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.
}
Pour résumer (après le commentaire d' Ogre Psalm33 ), RAII repose sur trois concepts:
Cela signifie que dans un code C ++ correct, la plupart des objets ne seront pas construits avec new
et seront déclarés sur la pile à la place. Et pour ceux construits en utilisant new
, tout sera en quelque sorte limité (par exemple attaché à un pointeur intelligent).
En tant que développeur, c'est en effet très puissant car vous n'aurez pas besoin de vous soucier de la gestion manuelle des ressources (comme cela se fait en C, ou pour certains objets en Java qui utilisent intensivement try
/ finally
pour ce cas) ...
"les objets à portée ... seront détruits ... peu importe la sortie" ce n'est pas tout à fait vrai. il existe des moyens de tromper RAII. toute saveur de terminate () contournera le nettoyage. exit (EXIT_SUCCESS) est un oxymore à cet égard.
wilhelmtell a tout à fait raison à ce sujet: il existe des moyens exceptionnels de tromper RAII, qui mènent tous à l'arrêt brutal du processus.
Ce sont des moyens exceptionnels car le code C ++ n'est pas jonché de terminate, exit, etc., ou dans le cas d'exceptions, nous voulons qu'une exception non gérée plante le processus et vider son image mémoire telle quelle, et non après le nettoyage.
Mais nous devons toujours être au courant de ces cas car, s'ils se produisent rarement, ils peuvent toujours se produire.
(qui appelle terminate
ou exit
en code C ++ occasionnel? ... Je me souviens avoir dû faire face à ce problème en jouant avec GLUT : Cette bibliothèque est très orientée C, allant jusqu'à la concevoir activement pour rendre les choses difficiles pour les développeurs C ++ comme ne pas s'en soucier sur les données allouées à la pile , ou avoir des décisions "intéressantes" pour ne jamais revenir de leur boucle principale ... je ne commenterai pas à ce sujet) .
Vous voudrez regarder des pointeurs intelligents, tels que les pointeurs intelligents de Boost .
Au lieu de
int main()
{
Object* obj = new Object();
//...
delete obj;
}
boost :: shared_ptr supprimera automatiquement une fois que le nombre de références est nul:
int main()
{
boost::shared_ptr<Object> obj(new Object());
//...
// destructor destroys when reference count is zero
}
Notez ma dernière remarque, "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 n'aurez pas à savoir si l'objet est toujours utilisé. Une fois que personne ne fait référence à 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 transmettre à une API tierce à moins d'être sûr de ce qu'il faisait. Souvent, votre "publication" de trucs sur un autre thread pour que le travail soit fait APRÈS que la création de la portée soit terminée. Ceci est courant 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 capuchon de réflexion avec n'importe quel outil ...
La plupart des fuites de mémoire sont le résultat d'un manque de clarté sur 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 allouer un seul objet dans un but précis.
Si vous avez besoin de «nouveau» un objet, la plupart du temps, il aura un seul propriétaire évident pour le reste de sa vie. Pour cette situation, j'ai tendance à utiliser un tas de modèles de collections conçus pour «posséder» des objets qui y sont stockés par pointeur. Ils sont implémentés avec le vecteur STL et les conteneurs cartographiques mais présentent quelques différences:
Mon point de vue 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 qui n'ont pas de sémantique de copie significative requise pour une utilisation dans ces conteneurs.
Bah, vous les jeunes enfants et vos nouveaux éboueurs ...
Règles très strictes sur la «propriété» - quel objet ou partie du logiciel a le droit de supprimer l'objet. Des commentaires clairs et des noms de variables sages pour rendre évident si un pointeur "possède" ou est "regarde, ne touche pas". Pour aider à décider qui possède quoi, suivez autant que possible le modèle «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 difficile d'éviter cela.
Dans tout programme nécessitant des structures de données complexes, je crée un arbre strict et clair d'objets contenant d'autres objets - en utilisant des pointeurs "propriétaire". Cet arbre 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 balayer des arays ou autre; ce sont les "juste à la recherche". Pour l'exemple de la scène 3D - un objet utilise une texture mais ne la possède pas; d'autres objets peuvent utiliser la même texture. La destruction d'un objet ne pas invoquer 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 l'arène limitée des logiciels scientifiques, d'acquisition de données et de graphiques haute performance. Je ne traite pas souvent des transactions comme dans la banque et le commerce électronique, des interfaces graphiques basées sur les événements ou un chaos asynchrone en réseau élevé. Peut-être que les nouvelles méthodes ont un avantage là-bas!
Excellente question!
si vous utilisez c ++ et que vous développez une application boudeur CPU-et-mémoire en temps réel (comme des jeux), vous devez écrire votre propre gestionnaire de mémoire.
Je pense que le mieux que vous puissiez faire est de fusionner des œuvres intéressantes de divers auteurs, je peux vous donner un indice:
L'allocateur de taille fixe est fortement discuté, partout sur le net
Small Object Allocation a été introduit par Alexandrescu en 2001 dans son livre parfait "Modern c ++ design"
Une grande avancée (avec le code source distribué) peut être trouvée 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 cet article
Ne commencez pas par vous-même à écrire un allocateur inutile ... DOCUMENTER VOUS-MÊME d'abord.
Une technique qui est devenue populaire avec la gestion de la mémoire en C ++ est RAII . Fondamentalement, vous utilisez des constructeurs / destructeurs pour gérer l'allocation des ressources. Bien sûr, il y a d'autres détails désagréables dans 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 à un problème de propriété. Je recommande vivement de lire la série Effective C ++ de Scott Meyers et Modern C ++ Design par 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 œil à:
Partagez et connaissez les règles de propriété de la mémoire dans votre projet. L'utilisation des règles COM assure la meilleure cohérence (les paramètres [in] appartiennent à l'appelant, l'appelé doit copier; les paramètres [out] appartiennent à l'appelant, l'appelé doit faire une copie s'il garde 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 utilisez pour écrire des tests unitaires pour vos programmes, vous devriez prendre l'habitude d'exécuter 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 identifier dans des tests simples que dans un logiciel complet.
Bien entendu, ces conseils restent valables pour tout autre outil de vérification de la mémoire.
Si vous ne pouvez pas / ne pas utiliser un pointeur intelligent pour quelque chose (bien que 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 saisir avant de taper du code dans la portée
Une source fréquente de ces bogues est lorsque vous avez une méthode qui accepte une référence ou un pointeur vers un objet mais laisse la propriété floue. Les conventions de style et de commentaire peuvent rendre cela moins probable.
Soit le cas où la fonction prend possession de l'objet soit le cas particulier. Dans toutes les situations où cela se produit, assurez-vous d'écrire un commentaire à côté de la fonction dans le fichier d'en-tête indiquant cela. Vous devez 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 aider beaucoup dans certains cas. Si une fonction ne modifie pas un objet et ne stocke pas de référence à celui-ci qui persiste après son retour, acceptez une référence const. En lisant le code de l'appelant, il sera évident que votre fonction n'a pas accepté la propriété de l'objet. Vous auriez pu avoir la même fonction accepter un pointeur non-const, et l'appelant peut ou non avoir supposé que l'appelé acceptait la propriété, mais avec une référence const, il n'y a pas de question.
N'utilisez pas de références non const dans les listes d'arguments. Il est très difficile de savoir lors de la lecture du code de l'appelant que l'appelé peut avoir conservé une référence au paramètre.
Je ne suis pas d'accord avec les commentaires recommandant des pointeurs de référence. Cela fonctionne généralement bien, mais lorsque 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 certainement d'ajuster votre conception pour ne pas avoir besoin de comptage de références si ce n'est pas trop difficile.
Conseils par ordre d'importance:
-Tip # 1 N'oubliez jamais de déclarer vos destructeurs "virtuels".
-Tip # 2 Utilisez RAII
-Tip # 3 Utilisez les smartpointers de Boost
-Conseil n ° 4 N'écrivez pas vos propres Smartpointers bogués, utilisez boost (sur un projet sur lequel je suis en ce moment, je ne peux pas utiliser boost, et j'ai souffert d'avoir à déboguer mes propres pointeurs intelligents, je ne prendrais certainement pas la même route encore, mais encore une fois pour le moment je ne peux pas ajouter de boost à nos dépendances)
-Tip # 5 Si c'est quelque chose de critique occasionnel / non-performance (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 votre plate-forme de choix tel que l'en-tête "vld" de Visual Leak Detection
Si vous le pouvez, utilisez boost shared_ptr et auto_ptr standard C ++. Ceux-ci véhiculent une sémantique de propriété.
Lorsque vous renvoyez un auto_ptr, vous dites à l'appelant que vous lui donnez la propriété de la mémoire.
Lorsque vous renvoyez un shared_ptr, vous dites à l'appelant que vous y avez une référence et qu'il en fait partie, mais ce n'est pas uniquement sa responsabilité.
Cette sémantique s'applique également aux paramètres. Si l'appelant vous passe un auto_ptr, il vous donne la propriété.
D'autres ont mentionné des moyens d'éviter les fuites de mémoire en premier lieu (comme les 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 logiciel gratuit.
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 supérieur, vous serez informé de toute fuite lorsque votre programme se termine (il suit nouveau / suppression). C'est basique, mais cela m'a aidé dans le passé.
valgrind (disponible uniquement pour les plates-formes * nix) est un très bon vérificateur de mémoire
Si vous comptez gérer votre mémoire manuellement, vous avez deux cas:
Si vous devez enfreindre l'une de ces règles, veuillez la documenter.
Tout est question de propriété du pointeur.
Vous pouvez intercepter les fonctions d'allocation de mémoire et voir s'il y a des zones de mémoire non libérées à la sortie du programme (bien que cela ne convienne pas à toutes les 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 dans ce site [Débogage de l'allocation de mémoire en C ++] Remarque: Il existe une astuce pour l'opérateur de suppression qui ressemble également à 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 de 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 de signaler quel bloc de mémoire alloué n'a pas été «supprimé» en l'identifiant par nom de fichier et numéro de ligne, ce que je suppose que vous voulez.
Vous pouvez également essayer quelque chose comme BoundsChecker sous Visual Studio qui est assez intéressant et facile à utiliser.
Nous enveloppons 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 versions non déboguées! Cela prend un peu plus de mémoire pour faire cela mais les avantages l'emportent largement sur les coûts.
Cela présente trois avantages: tout d'abord, cela vous permet de suivre facilement et rapidement le code qui fuit, en effectuant des recherches rapides de 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 lorsqu'une limite a été écrasée en vérifiant que toutes les sentinelles sont intactes. Cela nous a sauvé de nombreuses fois en essayant de trouver ces plantages bien cachés ou faux pas de tableau. Le troisième avantage est de suivre l'utilisation de la mémoire pour voir qui sont les grands joueurs - une compilation de certaines descriptions dans un MemDump vous indique quand le `` son '' prend beaucoup plus de place que vous ne l'aviez prévu, par exemple.
C ++ est conçu RAII à l'esprit. Il n'y a vraiment pas de meilleur moyen de gérer la mémoire en C ++, je pense. Mais veillez à ne pas allouer de très gros morceaux (comme des objets tampons) sur la portée 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 conduit à toutes sortes de failles de sécurité.
L'un des seuls exemples d'allocation et de destruction à différents endroits est la création de threads (le paramètre que vous passez). 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;
}
Assez facile n'est-ce pas? En cas d'échec de la création du thread, la ressource sera libérée (supprimée) par auto_ptr, sinon la propriété sera transmise au thread. Que faire si le thread est si rapide qu'après sa création, il libère la ressource avant le
param.release();
est appelé dans la fonction / méthode principale? Rien! Parce que nous allons 'dire' à auto_ptr d'ignorer la désallocation. La gestion de la mémoire C ++ est-elle facile, n'est-ce pas? À votre santé,
Ema!
Gérez la mémoire de la même manière que vous gérez les autres ressources (descripteurs, fichiers, connexions db, sockets ...). GC ne vous aiderait pas non plus.
Exactement un retour de n'importe quelle fonction. De cette façon, vous pouvez faire une désallocation là-bas et ne jamais la manquer.
Sinon, il est trop facile de se tromper:
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.