Quels sont les conseils généraux pour s'assurer que je ne perds pas de mémoire dans les programmes C++ ? Comment puis-je savoir qui doit libérer la mémoire qui a été allouée dynamiquement ?
Réponses
Trop de publicités?Je suis tout à fait d'accord avec tous les conseils concernant le RAII et les pointeurs intelligents, mais j'aimerais également ajouter un conseil d'un niveau légèrement supérieur : la mémoire la plus facile à gérer est celle que vous n'avez jamais allouée. Contrairement à des langages comme C# et Java, où pratiquement tout est une référence, en C++, vous devez placer les objets sur la pile chaque fois que vous le pouvez. Comme l'ont souligné plusieurs personnes (dont le Dr Stroustrup), la principale raison pour laquelle le garbage collection n'a jamais été populaire en C++ est que le C++ bien écrit ne produit pas beaucoup de déchets en premier lieu.
N'écrivez pas
Object* x = new Object;
ou même
shared_ptr<Object> x(new Object);
alors que vous pouvez simplement écrire
Object x;
Utilice RAII
- Oubliez la collecte d'ordures (Utilisez plutôt RAII). Notez que même le Garbage Collector peut fuir (si vous oubliez de "null" certaines références en Java/C#), et que le Garbage Collector ne vous aidera pas à vous débarrasser des ressources (si vous avez un objet qui a acquis un handle sur 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 si vous n'utilisez pas le pattern "dispose" en C#).
- Oubliez la règle du "un retour par fonction". . C'est un bon conseil en C pour éviter les fuites, mais il est dépassé en C++ en raison de son utilisation des exceptions (utilisez plutôt RAII).
- Et pendant que le "modèle sandwich" est un bon conseil C, il est dépassée en C++ en raison de son utilisation des exceptions (utilisez plutôt RAII).
Ce post peut sembler répétitif, mais en C++, le modèle le plus basique à connaître est RAII .
Apprenez à utiliser les pointeurs intelligents, qu'ils proviennent de boost, de TR1 ou même du modeste (mais souvent assez efficace) auto_ptr (mais vous devez connaître ses limites).
RAII est la base de la sécurité des exceptions et de la gestion des ressources en C++, et aucun autre modèle (sandwich, etc.) ne vous donnera les deux (et la plupart du temps, il ne vous donnera rien).
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, il fonctionne tout simplement ! Ne pas acquérir de ressources dans le constructeur.
- La destruction d'objets est suffisante ! Libérer les ressources dans le destructeur.
- Tout est dans les lunettes ! 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 sort de la portée, quelle que soit la manière dont la sortie a lieu (retour, pause, exception, etc.).
Cela signifie que, dans un code C++ correct, la plupart des objets ne seront pas construits avec la fonction new
et seront déclarés sur la pile à la place. Et pour ceux qui sont construits en utilisant new
tout sera en quelque sorte scoped (par exemple, attaché à un pointeur intelligent).
En tant que développeur, c'est très puissant, car vous n'aurez pas à vous soucier de la gestion manuelle des ressources (comme c'est le cas en C ou, pour certains objets, en Java qui fait un usage intensif de la fonction try
/ finally
pour ce cas)...
Edit (2012-02-12)
"les objets scopés ... seront détruits ... quelle que soit la sortie" ce n'est pas tout à fait vrai. il y a des moyens de tromper RAII. n'importe quelle saveur de terminate() contournera le nettoyage. exit(EXIT_SUCCESS) est un oxymore à cet égard.
wilhelmtell a tout à fait raison à ce sujet : Il y a exceptionnel des moyens de tromper le RAII, tous conduisant à l'arrêt brutal du processus.
Ce sont exceptionnel parce que le code C++ n'est pas truffé de terminate, exit, etc. ou, dans le cas des exceptions, nous voulons une fonction exception non gérée pour faire planter le processus et le noyau vide son image mémoire telle quelle, et pas après le nettoyage.
Mais nous devons tout de même connaître ces cas car, s'ils sont rares, ils peuvent tout de même se produire.
(qui appelle terminate
o exit
dans du code C++ occasionnel ?... Je me souviens avoir été confronté à ce problème lorsque je jouais avec GLUT : 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 données allouées à la pile ou de prendre des décisions "intéressantes" concernant ne revenant jamais de leur boucle principale ... Je ne ferai pas de commentaire à ce sujet) .
Vous voudrez examiner 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 que le nombre de références sera é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 de zéro, ce qui est la partie la plus cool. Donc, si vous avez plusieurs utilisateurs de votre objet, vous n'aurez pas à garder la trace de 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'elle fait. Souvent, vous "envoyez" des informations à un autre thread pour qu'il effectue le travail APRÈS que la portée de création 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 chapeau de réflexion avec n'importe quel outil...
Au lieu de gérer la mémoire manuellement, essayez d'utiliser des pointeurs intelligents lorsque cela est possible.
Jetez un coup d'œil à la Boost lib , TR1 y pointeurs intelligents .
De plus, les pointeurs intelligents font maintenant partie de la norme C++ appelée C++11 .
- Réponses précédentes
- Plus de réponses