Pourquoi l'utilisation de «nouveau» provoque-t-elle des fuites de mémoire?


131

J'ai d'abord appris C #, et maintenant je commence avec C ++. Si je comprends bien, l'opérateur newen 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());

Réponses:


464

Qu'est-ce qui se passe

Lorsque vous écrivez, T t;vous créez un objet de type Tavec une durée de stockage automatique . Il sera nettoyé automatiquement lorsqu'il sera hors de portée.

Lorsque vous écrivez, new T()vous créez un objet de type Tavec une durée de stockage dynamique . Il ne sera pas nettoyé automatiquement.

neuf sans nettoyage

Vous devez lui passer un pointeur deletepour le nettoyer:

nouveau avec suppression

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 vers l'objet créé avec new, vous ne pouvez donc jamais le supprimer même si vous le souhaitez!

nouveau avec deref

Ce que tu devrais faire

Vous devriez préférer la durée de stockage automatique. Besoin d'un nouvel objet, é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 dynamique, stockez le pointeur vers 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

nouveau avec automatic_pointer

C'est un idiome courant qui porte le nom peu descriptif RAII ( Resource Acquisition Is Initialization ). Lorsque vous acquérez une ressource qui doit être nettoyée, vous la collez dans un objet de durée de stockage automatique afin que vous n'ayez pas à vous soucier de son nettoyage. Cela s'applique à n'importe quelle ressource, que ce soit la mémoire, les fichiers ouverts, les connexions réseau ou tout ce que vous voulez.

Cette automatic_pointerchose existe déjà sous diverses formes, je viens de la fournir à titre d'exemple. Une classe très similaire existe dans la bibliothèque standard appelée std::unique_ptr.

Il existe également un ancien (pré-C ++ 11) nommé, auto_ptrmais 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.


4
@ user1131997: heureux que vous ayez posé cette autre question. Comme vous pouvez le voir, ce n'est pas très facile à expliquer dans les commentaires :)
R. Martinho Fernandes

@ R.MartinhoFernandes: excellente réponse. Juste une question. Pourquoi avez-vous utilisé return by reference dans la fonction operator * ()?
Destructor le

@Destructor réponse tardive: D. Le retour par référence vous permet de modifier la pointee, vous pouvez donc faire, par exemple *p += 2, comme vous le feriez avec un pointeur normal. S'il ne retournait pas par référence, il n'imiterait pas le comportement d'un pointeur normal, ce qui est l'intention ici.
R. Martinho Fernandes

Merci beaucoup d'avoir conseillé de «stocker le pointeur vers l'objet alloué dans un objet de durée de stockage automatique qui le supprime automatiquement». Si seulement il y avait un moyen d'exiger des codeurs qu'ils apprennent ce modèle avant de pouvoir compiler un C ++!
Andy

35

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 deletela mémoire allouée:

delete object1;

En C ++, il existe 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, avec lesquels vous allouez newet avec lesquels vous devez vous libérer delete. (tout cela est grosso modo)

Pensez que vous devriez avoir un deletepour chaque objet alloué avec new.

ÉDITER

À bien y penser, object2cela 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 celui-ci:

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, puisqu'il otherest passé par référence, ce sera l'objet exact pointé par new B(). Par conséquent, obtenir son adresse &otheret supprimer le pointeur libérerait la mémoire.

Mais je ne saurais trop insister sur ce point, ne le faites pas. C'est juste ici pour faire un point.


2
Je pensais la même chose: nous pouvons le pirater pour ne pas fuir mais vous ne voudriez pas faire cela. object1 n'a pas à fuir non plus, car son constructeur pourrait s'attacher à une sorte de structure de données qui la supprimera à un moment donné.
CashCow

2
C'est toujours tellement tentant d'écrire ces réponses "c'est possible de faire ça mais pas"! :-) Je connais le sentiment
Kos

11

É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" pointeurs 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();

Il s'agit d'attribuer la valeur de new A()à object1. La valeur est un pointeur, signifiant object1 == new A(), mais &object1 != &(new A()). (Notez que cet exemple n'est pas un code valide, c'est uniquement à titre d'explication)

Comme 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 celui 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 vers la mémoire allouée et nous ne pouvons donc pas la libérer. delete &object2;peut sembler fonctionner, mais parce que &object2 != &*(new A())ce n'est pas équivalent delete (new A())et donc invalide.


9

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 ++ a également un mot-clé "new" qui crée un objet mais contrairement à Java ou C #, ce n'est pas le seul moyen de créer un objet.

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

  • automatique
  • dynamique

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 la créeriez de cette façon:

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

Dans une classe, vous le créez 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 peut ê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 est membre.

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 de ces objets est un shared_ptr qui invoquera une logique "deleter" mais uniquement lorsque toutes les instances du shared_ptr qui partagent l'objet sont détruites.

En général, alors que votre code peut avoir de nombreux appels à new, vous devriez avoir des appels limités à supprimer et vous devez toujours vous assurer que ceux-ci sont appelés à partir de destructeurs ou d'objets "deleter" 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.


4
Il y a plus que automaticet dynamic. Il y a aussi static.
Mooing Duck

9
B object2 = *(new B());

Cette ligne est la cause de la fuite. Essayons de séparer cela un peu.

object2 est une variable de type B, stockée à l'adresse 1 par exemple (Oui, je choisis des nombres arbitraires ici). 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 dans l'adresse 3. Maintenant, le seul moyen d'accéder aux données de l'adresse 2 est d'utiliser le pointeur de l'adresse 3. Ensuite, vous avez déréférencé le pointeur en utilisant* 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 les affecte à object2, attribué à 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 adresse3, que vous avez utilisé pour accéder à adresse2) est hors de portée et hors de votre portée! Vous ne pouvez plus appeler la suppression sur celui-ci et ne pouvez donc pas nettoyer la mémoire dans address2. Il vous reste une copie des données de l'adresse2 dans l'adresse1. Deux des mêmes choses en mémoire. L'un auquel vous pouvez accéder, l'autre vous ne pouvez pas (parce que vous avez perdu le chemin). C'est pourquoi il s'agit d'une fuite de mémoire.

Je suggérerais, en venant de votre arrière-plan C #, que vous lisiez beaucoup sur le fonctionnement des pointeurs en C ++. Ils sont un sujet avancé et peuvent prendre un certain temps à appréhender, mais leur utilisation vous sera inestimable.


8

Si cela vous facilite la tâche, considérez la mémoire informatique comme un hôtel et les programmes sont des clients qui louent des chambres quand ils en ont besoin.

La façon dont cet hôtel fonctionne est que vous réservez une chambre et prévenez le portier de votre départ.

Si vous programmez réserver une chambre et quittez sans en informer le portier, celui-ci pensera que la chambre 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 cesse 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.


5
J'aime assez cette analogie, ce n'est pas parfait, mais c'est certainement un bon moyen d'expliquer les fuites de mémoire aux personnes qui en sont nouvelles!
AdamM

1
J'ai utilisé cela dans une interview pour un ingénieur senior chez Bloomberg à Londres pour expliquer les fuites de mémoire à une fille des RH. J'ai réussi cette interview parce que j'étais en mesure d'expliquer les fuites de mémoire (et les problèmes de threading) à un non programmeur d'une manière qu'elle comprenait.
Stefan

7

Lors de la création, object2vous 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 object2une référence.


3
C'est une pratique extrêmement mauvaise de prendre l'adresse d'une référence pour supprimer un objet. Utilisez un pointeur intelligent.
Tom Whittock

3
Une pratique incroyablement mauvaise, hein? Que pensez-vous que les pointeurs intelligents utilisent dans les coulisses?
Blindy

3
Les pointeurs intelligents @Blindy (au moins ceux mis en œuvre de manière décente) utilisent directement les pointeurs.
Luchian Grigore

2
Eh bien, pour être parfaitement honnête, toute l'idée n'est pas géniale, n'est-ce pas? En fait, je ne sais même pas où le modèle essayé dans l'OP serait réellement utile.
Mario

7

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' newopérateur en passant un pointeur vers cette mémoire à l' deleteopérateur.

Dans vos deux cas ci-dessus:

A *object1 = new A();

Ici, vous n'utilisez pas deletepour libérer de la mémoire, donc si et quand votre object1pointeur est hors de portée, vous aurez une fuite de mémoire, car vous aurez perdu le pointeur et ne pourrez donc pas utiliser l' deleteopérateur dessus.

Et ici

B object2 = *(new B());

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


7

C'est cette ligne qui fuit immédiatement:

B object2 = *(new B());

Ici, vous créez un nouveau B objet 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 deleten'as- object1bien.


4
Veuillez ne pas utiliser de tas / pile pour expliquer le stockage dynamique / automatique.
Pubby le

2
@Pubby pourquoi ne pas utiliser? Parce que le stockage dynamique / automatique est toujours en tas, pas en pile? Et c'est pourquoi il n'est pas nécessaire de détailler les piles / tas, n'est-ce pas?

4
@ user1131997 Heap / stack sont des détails d'implémentation. Ils sont importants à connaître, mais ne sont pas pertinents pour cette question.
Pubby

2
Hmm, j'aimerais une réponse distincte, c'est-à-dire la même que la mienne, mais en remplaçant le tas / pile par ce que vous pensez le mieux. Je serais intéressé de savoir comment vous préféreriez l'expliquer.
mattjgalloway
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.