Comment libérer correctement la mémoire allouée par placement nouveau?

J’ai lu quelque part que lorsque vous utilisez le placement nouveau, vous devez appeler le destructeur manuellement.

Considérons le code suivant:

// Allocate memory ourself char* pMemory = new char[ sizeof(MyClass)]; // Construct the object ourself MyClass* pMyClass = new( pMemory ) MyClass(); // The destruction of object is our duty. pMyClass->~MyClass(); 

Pour autant que je sache, l’opérateur delete appelle normalement le destructeur, puis libère la mémoire, non? Alors pourquoi n’utilisons-nous pas delete place?

 delete pMyClass; //what's wrong with that? 

dans le premier cas, nous sums obligés de définir pMyClass sur nullptr après avoir appelé destructor comme ceci:

 pMyClass->~MyClass(); pMyClass = nullptr; // is that correct? 

MAIS le destructeur n’a pas libéré la mémoire, non? Alors, s’agirait-il d’une fuite de mémoire?

Je suis confus, pouvez-vous expliquer cela?

L’utilisation de l’opérateur new deux effets: il appelle l’ operator new fonction operator new qui alloue de la mémoire, puis il utilise le placement new pour créer l’object dans cette mémoire. L’opérateur delete appelle le destructeur de l’object, puis appelle l’ operator delete . Oui, les noms sont déroutants.

 //normal version calls these two functions MyClass* pMemory = new MyClass; void* pMemory = operator new(sizeof(MyClass)); MyClass* pMyClass = new( pMemory ) MyClass(); //normal version calls these two functions delete pMemory; pMyClass->~MyClass(); operator delete(pMemory); 

Étant donné que dans votre cas, vous avez utilisé le placement nouveau manuellement, vous devez également appeler le destructeur manuellement. Comme vous avez alloué manuellement la mémoire, vous devez la libérer manuellement. Rappelez-vous que si vous allouez avec new , il doit y avoir une delete . Toujours.

Cependant, le placement nouveau est conçu pour fonctionner également avec les tampons internes (et d’autres scénarios), les tampons n’ayant pas été alloués avec l’ operator new , c’est pourquoi vous ne devez pas appeler l’ operator delete sur eux.

 #include  struct buffer_struct { std::aligned_storage::type buffer; }; int main() { buffer_struct a; MyClass* pMyClass = new (&a.buffer) MyClass(); //created inside buffer_struct a //stuff pMyClass->~MyClass(); //can't use delete, because there's no `new`. return 0; } 

Le but de la classe buffer_struct est de créer et de détruire le stockage de quelque manière que ce soit, tandis que main s’occupe de la construction / destruction de MyClass , notez à quel point les deux sont (presque *) complètement séparés l’un de l’autre.

* nous devons nous assurer que le stockage doit être suffisamment grand

Une des raisons pour lesquelles c’est faux:

 delete pMyClass; 

est-ce que vous devez supprimer pMemory avec delete[] puisqu’il s’agit d’un tableau:

 delete[] pMemory; 

Vous ne pouvez pas faire les deux choses ci-dessus.

De même, vous pourriez vous demander pourquoi vous ne pouvez pas utiliser malloc() pour allouer de la mémoire, placer pour la première fois pour construire un object, puis delete pour supprimer et libérer la mémoire. La raison en est que vous devez faire correspondre malloc() et free() , et non pas malloc() et delete .

Dans le monde réel, les appels de destructeurs nouveaux et explicites ne sont presque jamais utilisés. Ils peuvent être utilisés en interne par l’implémentation de la bibliothèque standard (ou pour une programmation au niveau système, comme indiqué dans les commentaires), mais les programmeurs normaux ne les utilisent pas. Je n’ai jamais utilisé de telles astuces pour le code de production depuis de nombreuses années en C ++.

Vous devez faire la distinction entre l’opérateur de delete et l’ operator delete . En particulier, si vous utilisez le placement new, vous appelez explicitement le destructeur puis appelez l’ operator delete (et non l’opérateur delete ) pour libérer la mémoire, c’est-à-dire

 X *x = static_cast(::operator new(sizeof(X))); new(x) X; x->~X(); ::operator delete(x); 

Notez que cela utilise l’ operator delete , qui est de niveau inférieur à l’opérateur delete et ne s’inquiète pas des destructeurs (c’est essentiellement un peu comme free ). Comparez cela à l’opérateur de delete , qui équivaut en interne à l’appel du destructeur et à l’ operator delete appelant operator delete .

Il est intéressant de noter que vous n’avez pas besoin d’utiliser ::operator new et ::operator delete pour allouer et désallouer votre tampon – en ce qui concerne le placement new, la manière dont le tampon entre ou est détruit importe peu. L’essentiel est de séparer les problèmes d’allocation de mémoire et de durée de vie des objects.

Incidemment, une application possible de ceci pourrait être dans quelque chose comme un jeu, où vous voudrez peut-être allouer un gros bloc de mémoire à l’avance afin de gérer soigneusement votre utilisation de la mémoire. Vous construirez ensuite des objects dans la mémoire que vous avez déjà acquise.

Une autre utilisation possible serait dans un petit allocateur d’objects de taille fixe optimisé.

Il est probablement plus facile de comprendre si vous imaginez construire plusieurs objects MyClass dans un bloc de mémoire.

Dans ce cas, cela donnerait quelque chose comme:

  1. Allouer un bloc de mémoire géant à l’aide des nouveaux caractères char [10 * sizeof (MyClass)] ou malloc (10 * sizeof (MyClass))
  2. Utilisez placement new pour construire dix objects MyClass dans cette mémoire.
  3. Faire quelque chose.
  4. Appelez le destructeur de chacun de vos objects
  5. Libérez le gros bloc de mémoire en utilisant delete [] ou free ().

C’est le genre de chose que vous pourriez faire si vous écrivez un compilateur, un système d’exploitation, etc.

Dans ce cas, j’espère que la raison pour laquelle vous avez besoin d’étapes séparées “destructeur” et “suppression” est claire, car il n’y a aucune raison d’appeler supprimer. Cependant, vous devriez désallouer la mémoire de la manière que vous feriez normalement (free, delete, ne rien faire pour un tableau statique géant, quitter normalement si le tableau fait partie d’un autre object, etc., etc.), et si vous ne le faites pas ll sera divulgué.

Notez également que, comme Greg l’a dit, dans ce cas, vous ne pouvez pas utiliser delete, car vous avez alloué le tableau avec new []. Vous devez donc utiliser delete [].

Notez également que vous devez supposer que vous n’avez pas omis de supprimer delete pour MyClass. Dans le cas contraire, il fera quelque chose de totalement différent qui est presque certainement incompatible avec “nouveau”.

Donc, je pense que vous ne voulez pas appeler «supprimer» comme vous le décrivez, mais est-ce que cela pourrait fonctionner? Je pense que la question est fondamentalement la même que celle-ci: “J’ai deux types non liés qui n’ont pas de destructeurs. Puis-je créer un pointeur sur un type, puis supprimer cette mémoire via un pointeur sur un autre type?” En d’autres termes, “mon compilateur a-t-il une grande liste de tous les éléments alloués, ou peut-il faire différentes choses pour différents types”.

J’ai bien peur de ne pas être sûr. En lisant les spécifications, il est écrit:

5.3.5 … Si le type statique de l’opérande [de l’opérateur de suppression] est différent de son type dynamic, le type statique doit être une classe de base du type dynamic de l’opérande et le type statique doit avoir un destructeur virtuel ou le le comportement est indéfini.

Je pense que cela signifie “Si vous utilisez deux types non liés, cela ne fonctionne pas (vous pouvez supprimer un object de classe de manière polymorphe via un destructeur virtuel)”.

Alors non, ne fais pas ça. Je suppose que cela peut souvent fonctionner dans la pratique si le compilateur regarde uniquement l’adresse et non le type (et que ni l’un ni l’autre n’est une classe à inheritances multiples, qui modifierait l’adresse), mais ne l’essayez pas.