Pourquoi l’utilisation de ‘new’ provoque-t-elle des memory leaks?

J’ai d’abord appris le C #, et maintenant je commence avec C ++. Si je comprends bien, l’opérateur new en C ++ n’est pas similaire à celui de C #.

Pouvez-vous expliquer la raison de la fuite de mémoire dans cet exemple de code?

 class A { ... }; struct B { ... }; A *object1 = new A(); B object2 = *(new B()); 

Qu’est-ce qui se passe

Lorsque vous écrivez T t; vous créez un object de type T avec une durée de stockage automatique . Il sera nettoyé automatiquement dès qu’il sera hors de scope.

Lorsque vous écrivez un new T() vous créez un object de type T avec une durée de stockage dynamic . Il ne sera pas nettoyé automatiquement.

nouveau sans nettoyage

Vous devez lui passer un pointeur pour le delete afin de le nettoyer:

newing avec supprimer

Cependant, votre deuxième exemple est pire: vous supprimez le pointeur et effectuez une copie de l’object. De cette façon, vous perdez le pointeur sur l’object créé avec new , vous ne pouvez donc jamais le supprimer même si vous le souhaitez!

nouveauté avec deref

Ce que tu devrais faire

Vous devriez préférer la durée de stockage automatique. Besoin d’un nouvel object, écrivez simplement:

 A a; // a new object of type A B b; // a new object of type B 

Si vous avez besoin d’une durée de stockage dynamic, stockez le pointeur sur l’object alloué dans un object de durée de stockage automatique qui le supprime automatiquement.

 template  class automatic_pointer { public: automatic_pointer(T* pointer) : pointer(pointer) {} // destructor: gets called upon cleanup // in this case, we want to use delete ~automatic_pointer() { delete pointer; } // emulate pointers! // with this we can write *p T& operator*() const { return *pointer; } // and with this we can write p->f() T* operator->() const { return pointer; } private: T* pointer; // for this example, I'll just forbid copies // a smarter class could deal with this some other way automatic_pointer(automatic_pointer const&); automatic_pointer& operator=(automatic_pointer const&); }; automatic_pointer a(new A()); // acts like a pointer, but deletes automatically automatic_pointer b(new B()); // acts like a pointer, but deletes automatically 

newing avec automatic_pointer

C’est un idiome commun qui s’appelle RAII ( Resource Acquisition Is Initialization ). Lorsque vous acquérez une ressource nécessitant un nettoyage, vous la placez dans un object dont la durée de stockage est automatique, vous n’avez donc pas à vous soucier de la nettoyer. Cela s’applique à toute ressource, que ce soit la mémoire, les fichiers ouverts, les connexions réseau ou tout ce que vous souhaitez.

Cette chose automatic_pointer existe déjà sous diverses formes, je viens de le fournir pour donner un exemple. Une classe très similaire existe dans la bibliothèque standard appelée std::unique_ptr .

Il y a aussi un ancien (pré-C ++ 11) nommé auto_ptr mais il est maintenant obsolète car il a un comportement de copie étrange.

Et puis, il y a des exemples encore plus intelligents, comme std::shared_ptr , qui autorisent plusieurs pointeurs sur le même object et ne le nettoient que lorsque le dernier pointeur est détruit.

Une explication étape par étape:

 // creates a new object on the heap: new B() // dereferences the object *(new B()) // calls the copy constructor of B on the object B object2 = *(new B()); 

Donc, à la fin de ceci, vous avez un object sur le tas sans pointeur vers lui, il est donc impossible de le supprimer.

L’autre échantillon:

 A *object1 = new A(); 

est une fuite de mémoire seulement si vous oubliez de delete la mémoire allouée:

 delete object1; 

En C ++, il existe des objects avec stockage automatique, ceux créés sur la stack, qui sont automatiquement supprimés, et des objects avec stockage dynamic, sur le tas, que vous allouez avec new et qui doivent vous libérer avec delete . (tout cela est grossièrement mis)

Pensez que vous devriez avoir une delete pour chaque object alloué avec new .

MODIFIER

En y pensant, object2 ne doit pas nécessairement être une fuite de mémoire.

Le code suivant est juste pour faire un point, c’est une mauvaise idée, ne jamais aimer le code comme ceci:

 class B { public: B() {}; //default constructor B(const B& other) //copy constructor, this will be called //on the line B object2 = *(new B()) { delete &other; } } 

Dans ce cas, puisque l’ other est passé par référence, ce sera l’object exact désigné par le new B() . Par conséquent, obtenir son adresse par &other et supprimer le pointeur libérerait la mémoire.

Mais je ne saurais trop insister là-dessus, ne le faites pas. C’est juste ici pour faire un point.

Étant donné deux “objects”:

 obj a; obj b; 

Ils n’occuperont pas le même emplacement en mémoire. En d’autres termes, &a != &b

L’atsortingbution de la valeur de l’un à l’autre ne changera pas leur emplacement, mais changera leur contenu:

 obj a; obj b = a; //a == b, but &a != &b 

Intuitivement, le pointeur “objects” fonctionne de la même manière:

 obj *a; obj *b = a; //a == b, but &a != &b 

Maintenant, regardons votre exemple:

 A *object1 = new A(); 

Ceci assigne la valeur de new A() à object1 . La valeur est un pointeur, ce object1 == new A() signifie object1 == new A() , mais &object1 != &(new A()) . (Notez que cet exemple n’est pas un code valide, il ne sert que d’explication)

Étant donné que la valeur du pointeur est préservée, nous pouvons libérer la mémoire vers laquelle il pointe: delete object1; En raison de notre règle, cela se comporte de la même manière que la delete (new A()); qui n’a pas de fuite.


Pour votre deuxième exemple, vous copiez l’object pointé. La valeur est le contenu de cet object, pas le pointeur réel. Comme dans tous les autres cas, &object2 != &*(new A()) .

 B object2 = *(new B()); 

Nous avons perdu le pointeur sur la mémoire allouée et nous ne pouvons donc pas le libérer. delete &object2; peut sembler fonctionner, mais parce que &object2 != &*(new A()) , il n’est pas équivalent à delete (new A()) et donc invalide.

En C # et Java, vous utilisez new pour créer une instance de n’importe quelle classe et vous n’avez plus besoin de la détruire ultérieurement.

C ++ a aussi un mot-clé “new” qui crée un object mais contrairement à Java ou à C #, ce n’est pas la seule façon de créer un object.

C ++ a deux mécanismes pour créer un object:

  • automatique
  • dynamic

Avec la création automatique, vous créez l’object dans un environnement défini: – dans une fonction ou – en tant que membre d’une classe (ou d’une structure).

Dans une fonction, vous le créeriez de cette façon:

 int func() { A a; B b( 1, 2 ); } 

Dans une classe, vous le feriez normalement de cette manière:

 class A { B b; public: A(); }; A::A() : b( 1, 2 ) { } 

Dans le premier cas, les objects sont automatiquement détruits à la sortie du bloc d’étendue. Cela pourrait être une fonction ou un bloc de scope dans une fonction.

Dans ce dernier cas, l’object b est détruit avec l’instance de A dans laquelle il est membre.

Les objects sont alloués avec new lorsque vous devez contrôler la durée de vie de l’object et nécessitent ensuite une suppression pour le détruire. Avec la technique connue sous le nom de RAII, vous prenez soin de supprimer l’object au moment où vous le créez en le plaçant dans un object automatique et attendez que le destructeur de cet object automatique prenne effet.

Un tel object est un shared_ptr qui invoquera une logique “deleter” mais uniquement lorsque toutes les instances du shared_ptr qui partagent l’object sont détruites.

En général, bien que votre code puisse comporter de nombreux appels vers de nouveaux appels, vous devriez avoir des appels limités à supprimer et vous devez toujours vous assurer qu’ils sont appelés par des destructeurs ou des objects “deleter” placés dans des smart-pointers.

Vos destructeurs ne devraient également jamais lancer d’exceptions.

Si vous faites cela, vous aurez peu de memory leaks.

 B object2 = *(new B()); 

Cette ligne est la cause de la fuite. Choisissons cela un peu à part ..

object2 est une variable de type B, stockée à l’adresse say 1 (oui, je choisis ici des nombres arbitraires). Sur le côté droit, vous avez demandé un nouveau B ou un pointeur sur un object de type B. Le programme vous le donne volontiers et atsortingbue votre nouveau B à l’adresse 2 et crée également un pointeur dans l’adresse 3. Maintenant, le seul moyen d’accéder aux données de l’adresse 2 se fait via le pointeur de l’adresse 3. Ensuite, vous avez déréférencé le pointeur à l’aide de * pour obtenir les données sur lesquelles pointe le pointeur (les données de l’adresse 2). Cela crée effectivement une copie de ces données et les assigne à object2, assigné dans l’adresse 1. Rappelez-vous, c’est une copie, pas l’original.

Maintenant, voici le problème:

Vous n’avez jamais stocké ce pointeur partout où vous pouvez l’utiliser! Une fois cette affectation terminée, le pointeur (mémoire dans l’adresse 3 que vous avez utilisée pour accéder à l’adresse 2) est hors de scope et hors de votre scope! Vous ne pouvez plus appeler la suppression sur celle-ci et ne pouvez donc pas nettoyer la mémoire dans l’adresse2. Ce qui vous rest est une copie des données de l’adresse2 dans l’adresse1. Deux des mêmes choses en mémoire. Vous pouvez y accéder l’un, l’autre que vous ne pouvez pas (parce que vous avez perdu le chemin). C’est pourquoi c’est une fuite de mémoire.

Je suggère de venir de votre arrière-plan C # que vous lisez beaucoup sur la façon dont les pointeurs dans C ++ fonctionnent. Ils sont un sujet avancé et peuvent prendre du temps à comprendre, mais leur utilisation vous sera précieuse.

Lors de la création de object2 vous créez une copie de l’object que vous avez créé avec new, mais vous perdez également le pointeur (jamais atsortingbué) (il est donc impossible de le supprimer ultérieurement). Pour éviter cela, vous devez faire de object2 une référence.

Eh bien, vous créez une fuite de mémoire si vous ne libérez pas à un moment donné la mémoire que vous avez allouée en utilisant le new opérateur en passant un pointeur sur cette mémoire à l’opérateur de delete .

Dans vos deux cas ci-dessus:

 A *object1 = new A(); 

Ici, vous n’utilisez pas delete pour libérer la mémoire, donc si et quand votre pointeur object1 est hors de scope, vous aurez une fuite de mémoire, car vous aurez perdu le pointeur et ne pourrez donc pas utiliser l’opérateur delete il.

Et ici

 B object2 = *(new B()); 

vous supprimez le pointeur renvoyé par le new B() , et ne pouvez donc jamais passer ce pointeur à delete pour que la mémoire soit libérée. D’où une autre fuite de mémoire.

C’est cette ligne qui fuit immédiatement:

 B object2 = *(new B()); 

Ici, vous créez un nouvel object B sur le tas, puis vous créez une copie sur la stack. Celui qui a été alloué sur le tas ne peut plus être consulté et donc la fuite.

Cette ligne ne fuit pas immédiatement:

 A *object1 = new A(); 

Il y aurait une fuite si vous ne delete jamais d object1 cependant.

Si cela facilite les choses, pensez à la mémoire informatique comme à un hôtel et les programmes sont des clients qui louent des salles quand ils en ont besoin.

La façon dont cet hôtel fonctionne est que vous réservez une chambre et dites au porteur quand vous partez.

Si vous programmez des livres dans une pièce et que vous partez sans prévenir le porteur, le portier pensera que la pièce est encore utilisée et ne laissera personne l’utiliser. Dans ce cas, il y a une fuite de pièce.

Si votre programme alloue de la mémoire et ne le supprime pas (il arrête simplement de l’utiliser), l’ordinateur pense que la mémoire est toujours utilisée et ne permet à personne de l’utiliser. Ceci est une fuite de mémoire.

Ce n’est pas une analogie exacte, mais cela pourrait aider.