Question

J'ai travaillé sur du code C ++ hérité qui utilise des structures à longueur variable (TAPI), dans lesquelles la taille de la structure dépend de chaînes de longueur variable. Les structures sont allouées par casting new :

STRUCT * pStruct = (STRUCT *) nouveau BYTE [sizeof (STRUCT) + nPaddingSize];

Toutefois, la mémoire est libérée ultérieurement à l'aide d'un appel delete :

delete pStruct;

Cette combinaison de tableau new [] et de non-tableau delete provoquera-t-elle une fuite de mémoire ou cela dépendra-t-il du compilateur? Aurais-je intérêt à changer ce code pour qu'il utilise plutôt malloc et free ?

Était-ce utile?

La solution

Techniquement, je pense que cela pourrait poser problème avec les allocateurs incompatibles, bien qu'en pratique je ne connaisse aucun compilateur qui ne ferait pas la bonne chose avec cet exemple.

Plus important encore, si STRUCT a un destructeur ou qu'il en ait déjà un, il invoquera le destructeur sans avoir appelé le constructeur correspondant.

Bien sûr, si vous savez d'où vient pStruct, pourquoi ne pas simplement le lancer en delete pour le faire correspondre à l'allocation:

delete [] (BYTE*) pStruct;

Autres conseils

Personnellement, je pense que vous feriez mieux d'utiliser std :: vector pour gérer votre mémoire. Vous n'avez donc pas besoin de delete .

std::vector<BYTE> backing(sizeof(STRUCT) + nPaddingSize);
STRUCT* pStruct = (STRUCT*)(&backing[0]);

Une fois que le support quitte la portée, votre pStruct n'est plus valide.

Ou, vous pouvez utiliser:

boost::scoped_array<BYTE> backing(new BYTE[sizeof(STRUCT) + nPaddingSize]);
STRUCT* pStruct = (STRUCT*)backing.get();

Ou boost :: shared_array si vous devez déplacer la propriété.

Oui, cela provoquera une fuite de mémoire.

Voir ceci sauf dans C ++ Gotchas: http://www.informit.com/articles/article.aspx?p=30642 pour pourquoi.

Raymond Chen explique en quoi les vecteurs new et delete diffèrent des versions scalaires sous les couvertures du compilateur Microsoft ... Ici: http://blogs.msdn.com/oldnewthing/archive/2004/02/03/66660.aspx

À mon humble avis, vous devriez corriger la suppression pour:

delete [] pStruct;

plutôt que de passer à malloc / free , ne serait-ce que parce qu'il est plus simple de procéder à une modification sans commettre d'erreur;)

Et, bien sûr, la modification la plus simple que je montre ci-dessus est fausse en raison de la répartition dans l'allocation d'origine, cela devrait être

delete [] reinterpret_cast<BYTE *>(pStruct);

alors, je suppose qu'il est probablement aussi facile de passer à malloc / libre après tout;)

Le comportement du code n'est pas défini. Vous pouvez avoir de la chance (ou pas) et cela peut fonctionner avec votre compilateur, mais ce n'est pas un code correct. Cela pose deux problèmes:

  1. Le delete doit être un tableau delete [] .
  2. Le delete doit être appelé par un pointeur du même type que le type alloué.

Donc, pour être tout à fait correct, vous voulez faire quelque chose comme ceci:

delete [] (BYTE*)(pStruct);

La norme C ++ indique clairement:

delete-expression:
             ::opt delete cast-expression
             ::opt delete [ ] cast-expression
  

La première alternative concerne les objets non-matrices et la seconde les matrices. L'opérande doit avoir un type de pointeur ou un type de classe possédant une seule fonction de conversion (12.3.2) en un type de pointeur. Le résultat a le type void.

     

Dans la première alternative (objet delete), la valeur de l'opérande delete doit être un pointeur sur un objet non-tableau. Dans le cas contraire, le comportement n'est pas défini.

La valeur de l'opérande dans delete pStruct est un pointeur sur un tableau de char , indépendant de son type statique ( STRUCT * ). . Par conséquent, toute discussion sur les fuites de mémoire est tout à fait inutile, car le code est mal formé, et un compilateur C ++ n’est pas nécessaire pour produire un exécutable raisonnable dans ce cas.

Il pourrait y avoir une fuite de mémoire, il ne pourrait pas, ou il pourrait tout faire pour planter votre système. En effet, une implémentation C ++ avec laquelle j'ai testé votre code interrompt l'exécution du programme au point de l'expression de suppression.

Comme souligné dans d'autres messages:

1) Les appels à nouveau / supprimer allouent de la mémoire et peuvent appeler des constructeurs / destructeurs (C ++ '03 5.3.4 / 5.3.5)

2) Le mélange de versions tableau / non tableau de nouveau et de delete constitue un comportement indéfini. (C ++ '03 5.3.5 / 4)

En regardant la source, il apparaît que quelqu'un a fait une recherche et a remplacé malloc et free et voici le résultat. C ++ remplace directement ces fonctions, à savoir appeler les fonctions d'allocation pour new et delete directement:

STRUCT* pStruct = (STRUCT*)::operator new (sizeof(STRUCT) + nPaddingSize);
// ...
pStruct->~STRUCT ();  // Call STRUCT destructor
::operator delete (pStruct);

Si le constructeur de STRUCT doit être appelé, vous pouvez envisager d'allouer la mémoire, puis d'utiliser l'emplacement nouveau :

.
BYTE * pByteData = new BYTE[sizeof(STRUCT) + nPaddingSize];
STRUCT * pStruct = new (pByteData) STRUCT ();
// ...
pStruct->~STRUCT ();
delete[] pByteData;

@eric - Merci pour les commentaires. Vous continuez cependant à dire quelque chose qui me rend dingue:

  

Ces bibliothèques d’exécution gèrent le   appels de gestion de la mémoire au système d'exploitation dans un   Syntaxe cohérente indépendante du système d'exploitation et   ces bibliothèques d'exécution sont   responsable de la fabrication malloc et nouvelle   travailler de manière cohérente entre les systèmes d'exploitation tels que   Linux, Windows, Solaris, AIX, etc.

Ce n'est pas vrai. Le rédacteur du compilateur assure la mise en œuvre des bibliothèques std, par exemple, et elles sont absolument libres de les mettre en œuvre de manière dépendante . Ils sont libres, par exemple, de faire un appel géant à malloc, puis de gérer la mémoire dans le bloc comme ils le souhaitent.

La compatibilité est fournie parce que l'API de std, etc. est identique, et non pas parce que toutes les bibliothèques d'exécution se retournent et appellent exactement les mêmes appels de système d'exploitation.

Les différentes utilisations possibles des mots-clés new et delete semblent créer une certaine confusion. Il existe toujours deux étapes pour construire des objets dynamiques en C ++: l’allocation de la mémoire brute et la construction du nouvel objet dans la zone mémoire allouée. De l’autre côté de la durée de vie de l’objet se trouvent la destruction de l’objet et la désaffectation de l’emplacement de mémoire où l’objet résidait.

Ces deux étapes sont fréquemment effectuées par une seule instruction C ++.

MyObject* ObjPtr = new MyObject;

//...

delete MyObject;

Au lieu de ce qui précède, vous pouvez utiliser les fonctions d'allocation de mémoire brute C ++ opérateur new et l'opérateur delete et la construction explicite (via placement nouveau ) et la destruction pour effectuer les étapes équivalentes.

void* MemoryPtr = ::operator new( sizeof(MyObject) );
MyObject* ObjPtr = new (MemoryPtr) MyObject;

// ...

ObjPtr->~MyObject();
::operator delete( MemoryPtr );

Notez qu’il n’ya pas de transtypage impliqué et que seul un type d’objet est construit dans la zone mémoire allouée. L'utilisation de quelque chose comme new char [N] comme moyen d'allouer de la mémoire brute est techniquement incorrecte dans la mesure où, logiquement, des objets char sont créés dans la mémoire récemment allouée. Je ne connais aucune situation dans laquelle cela ne fonctionne pas "tout simplement", mais la distinction entre allocation de mémoire brute et création d'objet est estompée, je vous le déconseille donc.

Dans ce cas particulier, il n'y a aucun avantage à séparer les deux étapes de delete , mais vous devez contrôler manuellement l'allocation initiale. Le code ci-dessus fonctionne dans le scénario "tout fonctionne", mais il perdra de la mémoire brute dans le cas où le constructeur de MyObject lève une exception. Bien que cela puisse être résolu et résolu avec un gestionnaire d’exceptions au moment de l’attribution, il est probablement plus judicieux de fournir un opérateur personnalisé new afin que la construction complète puisse être gérée par une nouvelle expression de placement.

class MyObject
{
    void* operator new( std::size_t rqsize, std::size_t padding )
    {
        return ::operator new( rqsize + padding );
    }

    // Usual (non-placement) delete
    // We need to define this as our placement operator delete
    // function happens to have one of the allowed signatures for
    // a non-placement operator delete
    void operator delete( void* p )
    {
        ::operator delete( p );
    }

    // Placement operator delete
    void operator delete( void* p, std::size_t )
    {
        ::operator delete( p );
    }
};

Il y a quelques points subtils ici. Nous définissons un nouvel emplacement de classe de manière à pouvoir allouer suffisamment de mémoire pour l'instance de classe, ainsi que certains bourrages spécifiables par l'utilisateur. Parce que nous faisons cela, nous devons fournir une suppression d’emplacement correspondant afin que, si l’allocation de mémoire réussit mais que la construction échoue, la mémoire allouée est automatiquement désallouée. Malheureusement, la signature de notre suppression de placement correspond à l'une des deux signatures autorisées pour la suppression de non-placement. Nous devons donc fournir l'autre forme de suppression de non-placement afin que notre suppression de placement réelle soit traitée comme une suppression de placement. (Nous aurions pu contourner ce problème en ajoutant un paramètre factice supplémentaire à la fois à notre nouvelle position et à notre suppression, mais cela aurait nécessité un travail supplémentaire sur tous les sites appelants.)

// Called in one step like so:
MyObject* ObjectPtr = new (padding) MyObject;

En utilisant une seule nouvelle expression, nous sommes maintenant assurés que la mémoire ne fuira pas si une partie de la nouvelle expression est renvoyée.

À l'autre bout de la durée de vie de l'objet, car nous avons défini l'opérateur delete (même si ce n'était pas le cas, la mémoire de l'objet provenait de l'opérateur global new dans tous les cas), voici la manière correcte de détruire le objet créé dynamiquement.

delete ObjectPtr;

Résumé!

  1. Ne regardez pas les moulages! opérateur new et opérateur delete traitent avec la mémoire brute, le placement nouveau peut construire des objets dans la mémoire brute. Un transtypage explicite d'un void * à un pointeur d'objet est généralement le signe d'un problème logique, même si cela ne fait que "fonctionner".

  2. Nous avons complètement ignoré new [] et supprimé []. Ces objets de taille variable ne fonctionneront jamais dans les tableaux.

  3. Le placement new permet à une nouvelle expression de ne pas fuir. La nouvelle expression est toujours considérée comme un pointeur sur un objet à détruire et une mémoire à désallouer. L'utilisation d'un type de pointeur intelligent peut aider à prévenir d'autres types de fuites. Du côté positif, nous avons laissé la méthode delete simple afin que la plupart des pointeurs intelligents fonctionnent.

Si vous voulez vraiment faire ce genre de chose, vous devriez probablement appeler l'opérateur nouveau directement:

STRUCT* pStruct = operator new(sizeof(STRUCT) + nPaddingSize);

Je crois que l'appel de cette façon évite d'appeler des constructeurs / destructeurs.

Je ne peux actuellement pas voter, mais La réponse de slicedlime est préférable à Réponse de Rob Walker , car le problème n'a rien à voir avec les allocateurs ni avec le fait que STRUCT ait ou non un destructeur.

Notez également que l'exemple de code ne provoque pas nécessairement une fuite de mémoire, mais bien un comportement indéfini. Presque tout peut arriver (de rien à un accident, très loin, très loin).

L'exemple de code entraîne un comportement indéfini, clair et simple. La réponse de slicedlime est directe et sans détour (avec l'avertissement que le mot "vecteur" devrait être remplacé par "tableau" car les vecteurs sont une chose de STL).

Ce genre de choses est assez bien traité dans la FAQ C ++ (sections 16.12, 16.13 et 16.14):

http://www.parashift.com /c++-faq-lite/freestore-mgmt.html#faq-16.12

Vous faites référence à une suppression de tableau ([]), pas à une suppression de vecteur. Un vecteur est std :: vector, il s’occupe de la suppression de ses éléments.

Vous pouvez renvoyer un BYTE * et supprimer:

delete[] (BYTE*)pStruct;

Oui, cela est possible, puisque vous allouez avec new [] mais désallouez avec delelte, oui malloc / free est plus sûr ici, mais en c ++, vous ne devez pas les utiliser car ils ne gèrent pas les constructeurs.

De plus, votre code appellera le déconstructeur, mais pas le constructeur. Cela peut causer une fuite de mémoire pour certaines structures (si le constructeur a alloué de la mémoire supplémentaire, par exemple pour une chaîne)

Il serait préférable de le faire correctement car cela appellera également correctement tous les constructeurs et déconstructeurs

STRUCT* pStruct = new STRUCT;
...
delete pStruct;

Il est toujours préférable de maintenir l'acquisition / la diffusion de toute ressource aussi équilibrée que possible. Bien que fuyant ou pas, il est difficile de dire dans ce cas. Cela dépend de la mise en oeuvre par le compilateur de la (dés) allocation de vecteur.

BYTE * pBytes = new BYTE [sizeof(STRUCT) + nPaddingSize];

STRUCT* pStruct = reinterpret_cast< STRUCT* > ( pBytes ) ;

 // do stuff with pStruct

delete [] pBytes ;

Len: le problème, c'est que pStruct est une STRUCT *, mais la mémoire allouée est en réalité un BYTE [] de taille inconnue. Donc, delete [] pStruct ne désaffectera pas toute la mémoire allouée.

Vous mélangez en quelque sorte les façons de faire en C et C ++. Pourquoi allouer plus que la taille d'une structure? Pourquoi ne pas simplement "nouvelle structure"? Si vous devez le faire, il pourrait être plus clair d’utiliser malloc et free dans ce cas, car vous ou les autres programmeurs pourriez être un peu moins susceptible de faire des hypothèses sur les types et les tailles des objets alloués.

@Matt Cruikshank Faites attention et lisez ce que j'ai écrit à nouveau car je n'ai jamais suggéré de ne pas appeler delete [] et de simplement laisser le système d'exploitation se nettoyer. Et vous vous trompez au sujet des bibliothèques d'exécution C ++ qui gèrent le tas. Si tel était le cas, le système d'exploitation C ++ ne serait pas portable tel qu'il est aujourd'hui et une application en panne ne serait jamais nettoyée par le système d'exploitation. (en reconnaissant que des temps d’exécution spécifiques à un système d’exploitation font que C / C ++ semble non portable). Je vous mets au défi de trouver stdlib.h dans les sources Linux de kernel.org. Le nouveau mot-clé en C ++ parle aux mêmes routines de gestion de la mémoire que malloc.

Les bibliothèques d'exécution C ++ effectuent des appels système et c'est le système d'exploitation qui gère les tas. Vous avez en partie raison en ce que les bibliothèques d'exécution indiquent quand libérer la mémoire, mais elles ne parcourent pas directement les tables de segment de mémoire. En d'autres termes, le moteur d'exécution auquel vous vous associez n'ajoute pas de code à votre application pour parcourir des tas à allouer ou désallouer. C'est le cas dans Windows, Linux, Solaris, AIX, etc. C'est aussi la raison pour laquelle vous ne raffinerez pas avec malloc dans les sources du noyau Linux, ni avec stdlib.h dans les sources Linux. Comprenez que ces systèmes d’exploitation modernes ont des gestionnaires de mémoire virtuelle qui compliquent un peu les choses.

Vous êtes-vous déjà demandé pourquoi vous pouvez appeler Malloc pour obtenir 2 Go de RAM sur une boîte 1G tout en récupérant un pointeur de mémoire valide?

La gestion de la mémoire sur les processeurs x86 est gérée dans l'espace noyau à l'aide de trois tables. PAM (table d'allocation de page), PD (répertoires de page) et PT (tables de page). C’est au niveau du matériel dont je parle. L'une des tâches du gestionnaire de mémoire du système d'exploitation, et non de votre application C ++, consiste à déterminer la quantité de mémoire physique installée sur la boîte lors du démarrage à l'aide d'appels du BIOS. Le système d'exploitation gère également des exceptions telles que, par exemple, lorsque vous essayez d'accéder à la mémoire, votre application ne dispose pas de droits également. (Erreur de protection générale GPF).

Il se peut que nous disions la même chose, Matt, mais je pense que vous confondez un peu la fonctionnalité du capot. Je maintiens un compilateur C / C ++ pour gagner sa vie ...

@ericmayo - cripes. En expérimentant avec VS2005, je ne peux obtenir une fuite honnête de la suppression scalaire sur la mémoire créée par vector new. Je suppose que le comportement du compilateur est " non défini " Ici, il s'agit de la meilleure défense que je puisse rassembler.

Vous devez cependant admettre que c’est une pratique vraiment moche de faire ce que disait l’affiche originale.

  

Si c'était le cas, alors C ++   ne pas être portable tel qu'il est aujourd'hui et   l'application se brisant ne serait jamais   nettoyé par l'OS.

Cette logique ne tient pas vraiment, cependant. Mon affirmation est que l'exécution d'un compilateur peut gérer la mémoire dans les blocs de mémoire que le système d'exploitation lui renvoie. C’est ainsi que fonctionnent la plupart des machines virtuelles. Par conséquent, votre argument contre la portabilité dans ce cas n’a pas beaucoup de sens.

@Matt Cruikshank

"En expérimentant avec VS2005, je ne parviens pas à obtenir une fuite honnête en suppression scalaire sur la mémoire créée par vector new. Je suppose que le comportement du compilateur est " non défini " Ici, il s’agit de la meilleure défense que je puisse rassembler. "

Je ne suis pas d'accord pour dire qu'il s'agit d'un comportement du compilateur ou même d'un problème de compilateur. Le nouveau mot clé est compilé et lié, comme vous l'avez indiqué, à des bibliothèques d'exécution. Ces bibliothèques d'exécution gèrent les appels de gestion de la mémoire au système d'exploitation dans une syntaxe cohérente indépendante du système d'exploitation. Ces bibliothèques d'exécution sont chargées de faire en sorte que malloc et les nouveaux fonctionnent de manière cohérente entre systèmes d'exploitation tels que Linux, Windows, Solaris, AIX, etc. C’est la raison pour laquelle j’ai mentionné l’argument de la transférabilité; une tentative de vous prouver que l'exécution ne gère pas non plus réellement la mémoire.

Le système d'exploitation gère la mémoire.

L’interface libs d’exécution avec le système d’exploitation .. Sous Windows, il s'agit des DLL du gestionnaire de mémoire virtuelle. C'est pourquoi stdlib.h est implémenté dans les bibliothèques GLIB-C et non dans la source du noyau Linux; Si GLIB-C est utilisé sur d’autres systèmes d’exploitation, l’implémentation des modifications de malloc permet d’appeler correctement le système d’exploitation. Dans VS, Borland, etc., vous ne trouverez jamais de bibliothèques fournies avec leurs compilateurs qui gèrent réellement la mémoire. Cependant, vous trouverez des définitions spécifiques au système d’exploitation pour malloc.

Puisque nous avons le code source de Linux, vous pouvez aller voir comment malloc y est implémenté. Vous verrez que malloc est réellement implémenté dans le compilateur GCC qui, à son tour, effectue essentiellement deux appels système Linux dans le noyau pour allouer de la mémoire. Jamais, malloc lui-même, en réalité gérer la mémoire!

Et ne me le prenez pas. Lisez le code source du système d’exploitation Linux ou voyez ce que K & amp; R en dit. Voici un lien PDF vers K & amp; R sur C.

http://www.oberon2005.ru/paper/kr_c.pdf

Voir vers la fin de la page 149: "Les appels gratuits et gratuits vers malloc peuvent survenir dans n'importe quel ordre; appels malloc sur le système d'exploitation pour obtenir plus de mémoire si nécessaire. Ces routines illustrent certaines des considérations liées à l’écriture de code dépendant de la machine d’une manière relativement indépendante de la machine, et illustrent également une application réelle des structures, des unions et de la typedef. "

"Vous devez admettre que c’est une pratique vraiment moche de faire ce que dit l’affiche originale."

Oh, je ne suis pas en désaccord là-bas. Mon point était que le code de l'affiche originale n'était pas propice à une fuite de mémoire. C'est tout ce que je disais. Je n'ai pas parlé des meilleures pratiques. Le code appelant delete, la mémoire est en train de se libérer.

Je suis d’accord, pour votre défense, si le code de l’affiche originale ne s’est jamais arrêté ou n’a jamais été transféré à l’appel de suppression, que le code pourrait avoir une fuite de mémoire, mais comme il dit que plus tard, il sera appelé la suppression. "Ensuite, la mémoire est libérée à l'aide d'un appel de suppression:"

De plus, la raison pour laquelle je répondais était due au commentaire de l'OP "Structures à longueur variable (TAPI), où la taille de la structure dépend de chaînes de longueur variable"

Ce commentaire donnait l’impression qu’il remettait en question la nature dynamique des attributions par rapport au casting et qu’il se demandait par conséquent si cela causerait une fuite de mémoire. Je lisais entre les lignes si vous voulez;).

Outre les excellentes réponses ci-dessus, j'aimerais également ajouter:

Si votre code fonctionne sur linux ou si vous pouvez le compiler, je vous suggère de l'exécuter via Valgrind . . C'est un excellent outil. Parmi la multitude d'avertissements utiles qu'il génère, il vous dira également quand vous allouez de la mémoire sous forme de tableau, puis que vous le libérez comme non-tableau (et inversement).

Utilisez l'opérateur new et delete:

struct STRUCT
{
  void *operator new (size_t)
  {
    return new char [sizeof(STRUCT) + nPaddingSize];
  }

  void operator delete (void *memory)
  {
    delete [] reinterpret_cast <char *> (memory);
  }
};

void main()
{
  STRUCT *s = new STRUCT;
  delete s;
}

Je pense qu'il n'y a pas de fuite de mémoire.

STRUCT* pStruct = (STRUCT*)new BYTE [sizeof(STRUCT) + nPaddingSize];

Ceci est traduit en un appel d'allocation de mémoire au sein du système d'exploitation sur lequel un pointeur sur cette mémoire est renvoyé. Au moment où la mémoire est allouée, la taille de sizeof (STRUCT) et la taille de nPaddingSize sont connues afin de répondre à toute demande d'allocation de mémoire par rapport au système d'exploitation sous-jacent.

La mémoire allouée est donc "enregistrée". dans les tables d'allocation de mémoire globale du système d'exploitation. Les tables de mémoire sont indexées par leurs pointeurs. Ainsi, dans l'appel à supprimer correspondant, toute la mémoire allouée à l'origine est libre. (la fragmentation de la mémoire est également un sujet populaire dans ce domaine).

Vous voyez, le compilateur C / C ++ ne gère pas de mémoire, le système d'exploitation sous-jacent est.

Je conviens qu'il existe des méthodes plus propres, mais le PO a indiqué qu'il s'agissait d'un code hérité.

En bref, je ne vois pas de fuite de mémoire car la réponse acceptée pense qu’il en existe une.

Rob Walker répondre c'est bien.

Juste un petit ajout, si vous n'avez pas de constructeur et / ou de distracteur, vous avez donc besoin d'allouer et de libérer un bloc de mémoire brute, envisagez d'utiliser paire libre / malloc.

ericmayo.myopenid.com a tellement tort qu’une personne suffisamment réputée devrait lui accorder un vote négatif.

Les bibliothèques d'exécution C ou C ++ gèrent le tas qui lui est attribué par blocs par le système d'exploitation, un peu comme vous l'indiquez, Eric. Mais il incombe au développeur d'indiquer au compilateur quels appels d'exécution doivent être effectués pour libérer de la mémoire et éventuellement détruire les objets qui s'y trouvent. La suppression de vecteur (aka delete []) est nécessaire dans ce cas, pour que le runtime C ++ laisse le segment de mémoire dans un état valide. Le fait que, lorsque le processus se termine, le système d'exploitation est suffisamment intelligent pour libérer les blocs de mémoire sous-jacents n'est pas une chose sur laquelle les développeurs devraient s'appuyer. Ce serait comme ne jamais appeler du tout supprimer.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top