J'ai d'abord appris C #, et maintenant je commence par C++. Si je comprends bien, l'opérateur new
en C++ n'est pas similaire à celui en 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());
ce qui se passe
Lorsque vous écrivez T t;
, Vous créez un objet de type T
avec durée de stockage automatique . Il sera nettoyé automatiquement lorsqu'il sortira du cadre.
Lorsque vous écrivez new T()
vous créez un objet de type T
avec durée de stockage dynamique . Il ne sera pas nettoyé automatiquement.
Vous devez lui passer un pointeur sur delete
pour le nettoyer:
Cependant, votre deuxième exemple est pire: vous déréférencer le pointeur et faire une copie de l'objet. De cette façon, vous perdez le pointeur sur l'objet créé avec new
, vous ne pouvez donc jamais le supprimer même si vous le souhaitez!
Ce que vous devez faire
Vous devriez préférer la durée de stockage automatique. Besoin d'un nouvel objet, il suffit d'écrire:
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 dynamique, stockez le pointeur sur l'objet alloué dans un objet de durée de stockage automatique qui le supprime automatiquement.
template <typename T>
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> a(new A()); // acts like a pointer, but deletes automatically
automatic_pointer<B> b(new B()); // acts like a pointer, but deletes automatically
Il s'agit d'un idiome courant qui porte le nom peu descriptif RAII ( L'acquisition de ressources est l'initialisation ). Lorsque vous acquérez une ressource qui nécessite un nettoyage, vous la collez dans un objet de durée de stockage automatique afin que vous n'ayez pas à vous soucier de la nettoyer. Cela s'applique à toutes les ressources, 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 la 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 vers le même objet 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 cela, vous avez un objet 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 uniquement si vous oubliez de delete
la mémoire allouée:
delete object1;
En C++, il y a des objets avec stockage automatique, ceux créés sur la pile, qui sont automatiquement supprimés, et des objets avec stockage dynamique, sur le tas, que vous allouez avec new
et que vous devez libérer avec delete
. (tout est grosso modo)
Pensez que vous devriez avoir un delete
pour chaque objet alloué avec new
.
MODIFIER
À bien y penser, object2
Ne doit pas être une fuite de mémoire.
Le code suivant est juste pour faire un point, c'est une mauvaise idée, n'aime jamais le code comme ça:
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 other
est passé par référence, ce sera l'objet exact pointé par new B()
. Par conséquent, obtenir son adresse en &other
Et supprimer le pointeur libérerait la mémoire.
Mais je ne saurais trop insister là-dessus, ne faites pas ça. Il est juste là pour faire valoir un point.
Étant donné deux "objets":
obj a;
obj b;
Ils n'occuperont pas le même emplacement en mémoire. En d'autres termes, &a != &b
Attribuer la valeur de l'un à l'autre ne changera pas leur emplacement, mais cela changera leur contenu:
obj a;
obj b = a;
//a == b, but &a != &b
Intuitivement, les "objets" de pointeur fonctionnent 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 affecte la valeur de new A()
à object1
. La valeur est un pointeur, ce qui signifie object1 == new A()
, mais &object1 != &(new A())
. (Notez que cet exemple n'est pas un code valide, c'est uniquement à des fins d'explication)
Parce 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 delete (new A());
qui n'a pas de fuite.
Pour votre deuxième exemple, vous copiez l'objet pointé. La valeur est le contenu de cet objet, 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.
B object2 = *(new B());
Cette ligne est à l'origine de la fuite. Choisissons ceci un peu à part ..
object2 est une variable de type B, stockée à l'adresse 1 par exemple (Oui, je prends ici des nombres arbitraires). Sur le côté droit, vous avez demandé un nouveau B, ou un pointeur vers un objet de type B. Le programme vous le donne volontiers et affecte votre nouveau B à l'adresse 2 et crée également un pointeur à l'adresse 3. Maintenant, le seul moyen d'accéder aux données de l'adresse 2 est via le pointeur de l'adresse 3. Ensuite, vous avez déréférencé le pointeur à l'aide de *
pour obtenir les données vers lesquelles le pointeur pointe (les données de l'adresse 2). Cela crée effectivement une copie de ces données et l'affecte à object2, affecté à l'adresse 1. Souvenez-vous, c'est une COPIE, pas l'original.
Maintenant, voici le problème:
Vous n'avez jamais réellement stocké ce pointeur partout où vous pouvez l'utiliser! Une fois cette affectation terminée, le pointeur (mémoire dans l'adresse3, que vous avez utilisée pour accéder à l'adresse2) est hors de portée et hors de votre portée! Vous ne pouvez plus appeler delete dessus et ne pouvez donc pas nettoyer la mémoire de l'adresse 2. Il vous reste une copie des données de l'adresse2 dans l'adresse1. Deux des mêmes choses en mémoire. L'un vous pouvez accéder, l'autre vous ne pouvez pas (parce que vous avez perdu le chemin d'accès). C'est pourquoi c'est une fuite de mémoire.
Je suggérerais de venir de votre arrière-plan C # que vous lisez beaucoup sur le fonctionnement des pointeurs en C++. Ils sont un sujet avancé et peuvent prendre un certain temps à comprendre, mais leur utilisation vous sera précieuse.
En C # et Java, vous utilisez new pour créer une instance de n'importe quelle classe et vous n'avez pas à vous soucier de la détruire plus tard.
C++ possède également un mot-clé "new" qui crée un objet mais contrairement à Java ou C #, ce n'est pas la seule façon de créer un objet.
C++ a deux mécanismes pour créer un objet:
Avec la création automatique, vous créez l'objet dans un environnement délimité: - dans une fonction ou - en tant que membre d'une classe (ou struct).
Dans une fonction, vous le créeriez de cette façon:
int func()
{
A a;
B b( 1, 2 );
}
Dans une classe, vous le créeriez normalement de cette façon:
class A
{
B b;
public:
A();
};
A::A() :
b( 1, 2 )
{
}
Dans le premier cas, les objets sont détruits automatiquement à la sortie du bloc de portée. Cela pourrait être une fonction ou un bloc de portée dans une fonction.
Dans ce dernier cas, l'objet b est détruit avec l'instance de A dont il fait partie.
Les objets sont alloués avec new lorsque vous devez contrôler la durée de vie de l'objet, puis il faut supprimer pour le détruire. Avec la technique connue sous le nom de RAII, vous vous occupez de la suppression de l'objet au moment où vous le créez en le plaçant dans un objet automatique, et attendez que le destructeur de cet objet automatique prenne effet.
Un tel objet est un shared_ptr qui invoquera une logique "deleter" mais uniquement lorsque toutes les instances de shared_ptr qui partagent l'objet sont détruites.
En général, bien que votre code puisse avoir de nombreux appels à new, vous devez avoir des appels limités à supprimer et vous devez toujours vous assurer qu'ils sont appelés à partir de destructeurs ou d'objets "deleter" qui sont placés dans des pointeurs intelligents.
Vos destructeurs ne devraient également jamais lancer d'exceptions.
Si vous faites cela, vous aurez peu de fuites de mémoire.
Si cela facilite les choses, pensez à la mémoire de l'ordinateur comme à un hôtel et les programmes sont des clients qui louent des chambres quand ils en ont besoin.
Le fonctionnement de cet hôtel est que vous réservez une chambre et prévenez le portier quand vous partez.
Si vous programmez la réservation d'une chambre et que vous partez sans en informer le portier, le portier pensera que la salle est toujours utilisée et ne laissera personne d'autre l'utiliser. Dans ce cas, il y a une fuite dans la pièce.
Si votre programme alloue de la mémoire et ne la supprime pas (il arrête simplement de l'utiliser), l'ordinateur pense que la mémoire est toujours utilisée et ne permettra à personne d'autre de l'utiliser. Il s'agit d'une fuite de mémoire.
Ce n'est pas une analogie exacte, mais cela pourrait aider.
Lors de la création de object2
vous créez une copie de l'objet que vous avez créé avec new, mais vous perdez également le pointeur (jamais attribué) (il n'y a donc aucun moyen de le supprimer plus tard). Pour éviter cela, vous devez faire object2
une référence.
C'est cette ligne qui fuit immédiatement:
B object2 = *(new B());
Ici, vous créez un nouvel objet B
sur le tas, puis créez une copie sur la pile. Celui qui a été alloué sur le tas n'est plus accessible et donc la fuite.
Cette ligne ne fuit pas immédiatement:
A *object1 = new A();
Il y aurait une fuite si vous ne delete
d object1
bien que.
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 à l'aide de l'opérateur new
en passant un pointeur vers cette mémoire à l'opérateur delete
.
Dans vos deux cas ci-dessus:
A *object1 = new A();
Ici, vous n'utilisez pas delete
pour libérer de la mémoire, donc si et quand votre pointeur object1
Devient hors de portée, vous aurez une fuite de mémoire, car vous aurez perdu le pointeur et ne peut donc pas utiliser l'opérateur delete
dessus.
Et ici
B object2 = *(new B());
vous supprimez le pointeur renvoyé par new B()
et ne pouvez donc jamais passer ce pointeur à delete
pour que la mémoire soit libérée. D'où une nouvelle fuite de mémoire.