web-dev-qa-db-fra.com

Est-ce une bonne pratique de NULL un pointeur après l'avoir supprimé?

Je commencerai par dire, utilisez des pointeurs intelligents et vous n'aurez plus jamais à vous en soucier.

Quels sont les problèmes avec le code suivant?

Foo * p = new Foo;
// (use p)
delete p;
p = NULL;

Cela a été déclenché par ne réponse et des commentaires à une autre question. Un commentaire de Neil Butterworth a généré quelques votes positifs:

La définition de pointeurs sur NULL après la suppression n'est pas une bonne pratique universelle en C++. Il y a des moments où c'est une bonne chose à faire et des moments où cela est inutile et peut cacher des erreurs.

Il y a beaucoup de circonstances dans lesquelles cela n'aiderait pas. Mais d'après mon expérience, ça ne peut pas faire de mal. Quelqu'un m'éclaire.

136
Mark Ransom

Définir un pointeur sur 0 (qui est "null" en C++ standard, la définition de NULL à partir de C est quelque peu différent) évite les plantages lors de doubles suppressions.

Considérer ce qui suit:

Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything

Tandis que:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 

En d'autres termes, si vous ne définissez pas les pointeurs supprimés sur 0, vous aurez des problèmes si vous effectuez des suppressions doubles. Un argument contre la définition de pointeurs sur 0 après la suppression serait que cela masquait simplement les bogues de suppression en double et les laissait non traités.

Évidemment, il est préférable de ne pas supprimer de bogues en double suppression, mais en fonction de la sémantique de la propriété et du cycle de vie des objets, cela peut être difficile à réaliser dans la pratique. Je préfère un bogue masqué double suppression sur UB.

Enfin, une note de bas de page concernant la gestion de l'allocation d'objets, je vous suggère de jeter un coup d'œil à std::unique_ptr pour propriété stricte/singulière, std::shared_ptr pour la propriété partagée ou une autre implémentation de pointeur intelligent, selon vos besoins.

79
Håvard S

Définir les pointeurs sur NULL après avoir supprimé ce qui y était indiqué ne peut certainement pas faire de mal, mais il s'agit souvent d'un pansement sur un problème plus fondamental: pourquoi utilisez-vous un pointeur en premier lieu? Je peux voir deux raisons typiques:

  • Vous vouliez simplement quelque chose alloué sur le tas. Dans ce cas, l'envelopper dans un objet RAII aurait été beaucoup plus sûr et plus propre. Terminez la portée de l'objet RAII lorsque vous n'avez plus besoin de cet objet. C'est comme ça std::vector fonctionne et résout le problème de laisser accidentellement des pointeurs sur la mémoire désallouée. Il n'y a pas d'indicateurs.
  • Ou peut-être souhaitiez-vous une sémantique complexe de propriété partagée. Le pointeur renvoyé par new risque de ne pas être identique à celui sur lequel delete est appelé. Plusieurs objets peuvent avoir utilisé l'objet simultanément entre-temps. Dans ce cas, un pointeur partagé ou quelque chose de similaire aurait été préférable.

Ma règle générale est que si vous laissez des pointeurs dans le code utilisateur, vous vous trompez. Le pointeur ne devrait pas être là pour indiquer des déchets. Pourquoi n'y a-t-il pas d'objet prenant la responsabilité d'en assurer la validité? Pourquoi sa portée ne se termine-t-elle pas lorsque l'objet pointé se termine?

55
jalf

J'ai une meilleure pratique encore meilleure: dans la mesure du possible, mettez fin à la portée de la variable!

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}
42
Don Neufeld

Je place toujours un pointeur sur NULL (maintenant nullptr) après avoir supprimé le ou les objets sur lesquels il pointe.

  1. Cela peut aider à attraper de nombreuses références à la mémoire libérée (en supposant que les erreurs de votre plate-forme reposent sur un pointeur nul).

  2. Toutes les références permettant de libérer de la mémoire ne seront pas capturées si, par exemple, des copies du pointeur traînent. Mais certains valent mieux que rien.

  3. Cela masquera une double suppression, mais je trouve que celles-ci sont beaucoup moins courantes que les accès à la mémoire déjà libérée.

  4. Dans de nombreux cas, le compilateur va l'optimiser. Donc, l'argument selon lequel c'est inutile ne me convainc pas.

  5. Si vous utilisez déjà RAII, il ne reste pas beaucoup de deletes dans votre code. L'argument voulant que l'affectation supplémentaire provoque un encombrement ne me convainc pas.

  6. Lors du débogage, il est souvent pratique de voir la valeur null plutôt qu'un pointeur périmé.

  7. Si cela vous dérange toujours, utilisez plutôt un pointeur intelligent ou une référence.

J'ai également défini d'autres types de descripteurs de ressource sur la valeur sans ressource lorsque la ressource est libre (qui est généralement uniquement dans le destructeur d'un encapsuleur RAII écrit pour encapsuler la ressource).

J'ai travaillé sur un produit commercial important (9 millions de relevés) (principalement en C). À un moment donné, nous avons utilisé la magie macro pour supprimer le pointeur lorsque la mémoire était libérée. Cela a immédiatement exposé beaucoup de bugs cachés qui ont été rapidement corrigés. Autant que je me souvienne, nous n'avons jamais eu de bogue à double libération.

Mise à jour: Microsoft considère que c'est une bonne pratique pour la sécurité et la recommande dans ses stratégies SDL. Apparemment, MSVC++ 11 va écraser le pointeur supprimé automatiquement (dans de nombreuses circonstances) si vous compilez avec l'option/SDL.

28
Adrian McCarthy

Tout d'abord, il existe de nombreuses questions sur ce sujet et sur des sujets étroitement liés, par exemple Pourquoi ne pas supprimer ne positionne-t-il le pointeur sur NULL? .

Dans votre code, indiquez ce qui se passe dans (utilisez p). Par exemple, si vous avez quelque part un code comme celui-ci:

Foo * p2 = p;

puis régler p sur NULL accomplit très peu, car vous avez toujours le pointeur p2 à traiter.

Cela ne veut pas dire que définir un pointeur sur NULL est toujours inutile. Par exemple, si p était une variable membre pointant sur une ressource dont la durée de vie n'était pas exactement la même que la classe contenant p, définir p sur NULL pourrait être un moyen utile d'indiquer la présence ou l'absence de la ressource.

12
anon

S'il y a plus de code après le delete, oui. Lorsque le pointeur est supprimé dans un constructeur ou à la fin d'une méthode ou d'une fonction, Non.

Le but de cette parabole est de rappeler au programmeur, pendant l'exécution, que l'objet a déjà été supprimé.

Une meilleure pratique consiste à utiliser des pointeurs intelligents (partagés ou étendus) qui suppriment automatiquement leurs objets cibles.

7
Thomas Matthews

Comme d'autres l'ont dit, delete ptr; ptr = 0; Ne fera pas voler les démons par le nez. Cependant, cela encourage l'utilisation de ptr comme drapeau. Le code est jonché de delete et le pointeur est placé sur NULL. L'étape suivante consiste à disperser if (arg == NULL) return; dans votre code pour vous protéger contre l'utilisation accidentelle d'un pointeur NULL. Le problème se produit lorsque les vérifications effectuées sur NULL deviennent votre principal moyen de vérification de l'état d'un objet ou d'un programme.

Je suis sûr qu'il y a une odeur de code à propos de l'utilisation d'un pointeur comme indicateur quelque part, mais je n'en ai pas trouvé.

3
D.Shawley

Si aucune autre contrainte ne vous oblige à définir ou non le pointeur sur NULL après l'avoir supprimée (une de ces contraintes a été mentionnée par Neil Butterworth ), ma préférence personnelle est de la laisser telle quelle. .

Pour moi, la question n'est pas "est-ce une bonne idée?" mais "quel comportement pourrais-je empêcher ou permettre de réussir en faisant cela?" Par exemple, si cela permet à un autre code de voir que le pointeur n'est plus disponible, pourquoi un autre code tente-t-il même de regarder les pointeurs libérés après leur libération? D'habitude, c'est un bug.

Il fait également plus de travail que nécessaire et empêche le débogage post-mortem. Moins vous effleurez la mémoire lorsque vous n'en avez plus besoin, plus il est facile de comprendre pourquoi quelque chose s'est écrasé. Bien souvent, je me suis appuyé sur le fait que la mémoire est dans un état similaire à celui où un bogue est survenu pour diagnostiquer et corriger ce bogue.

2
MSN

Annuler explicitement après suppression suggère fortement au lecteur que le pointeur représente quelque chose qui est conceptuellement optionnel. Si je voyais cela se faire, je commencerais à craindre que le pointeur s'habitue partout dans la source et qu'il devrait d'abord être testé contre NULL.

Si c'est ce que vous voulez dire, il est préférable de l'expliciter dans le source en utilisant quelque chose comme boost :: optional

optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

Mais si vous voulez vraiment que les gens sachent que le pointeur a "mal tourné", je suis tout à fait d’accord avec ceux qui disent que la meilleure chose à faire est de le faire sortir de son cadre. Ensuite, vous utilisez le compilateur pour éviter la possibilité de mauvaises déréférences lors de l'exécution.

C'est le bébé dans toute l'eau du bain C++, il ne faut pas le jeter. :)

2
HostileFork

Je vais changer légèrement votre question:

Voulez-vous utiliser un pointeur non initialisé? Vous savez, un que vous n'avez pas défini sur NULL ou alloué la mémoire à laquelle il pointe?

Il existe deux scénarios dans lesquels définir le pointeur sur NULL peut être ignoré:

  • la variable de pointeur sort immédiatement de la portée
  • vous avez surchargé la sémantique du pointeur et utilisez sa valeur non seulement comme pointeur de mémoire, mais également comme clé ou valeur brute. Cette approche souffre toutefois d'autres problèmes.

En attendant, affirmer que le fait de positionner le pointeur sur NULL pourrait me cacher des erreurs, cela revient à dire qu'il ne faut pas réparer un bogue, car le correctif pourrait en cacher un autre. Les seuls bogues pouvant apparaître si le pointeur n'est pas défini sur NULL sont ceux qui tentent d'utiliser le pointeur. Mais le régler sur NULL provoquerait exactement le même bogue que si vous l’utilisiez avec de la mémoire libérée, n’est-ce pas?

2
Franci Penov

Dans un programme bien structuré avec un contrôle d'erreur approprié, il n'y a aucune raison pas de l'affecter à une valeur nulle. 0 est la seule valeur invalide universellement reconnue dans ce contexte. Fail hard et Fail bientôt.

De nombreux arguments contre l’attribution de 0 suggère que cela pourrait cacher un bogue ou compliquer le flux de contrôle. Fondamentalement, il s’agit d’une erreur en amont (pas de votre faute (désolé pour le mauvais jeu de mots)) ou d’une autre erreur de la part du programmeur - peut-être même une indication que le déroulement du programme est devenu trop complexe.

Si le programmeur veut introduire l'utilisation d'un pointeur qui peut être nul en tant que valeur spéciale et écrire tous les contournements nécessaires autour de cela, c'est une complication qu'il a délibérément introduite. Plus la quarantaine est bonne, plus vite vous trouvez des cas de mauvaise utilisation et moins ils sont en mesure de se répandre dans d'autres programmes.

Des programmes bien structurés peuvent être conçus à l'aide de fonctionnalités C++ pour éviter ces cas. Vous pouvez utiliser des références ou simplement dire que "transmettre/utiliser des arguments nuls ou non valides est une erreur", approche qui s'applique également aux conteneurs, tels que les pointeurs intelligents. Augmenter le comportement cohérent et correct empêche ces bugs d'aller loin.

À partir de là, vous avez uniquement une portée et un contexte très limités dans lesquels un pointeur nul peut exister (ou est autorisé).

La même chose peut être appliquée aux pointeurs qui ne sont pas const. Suivre la valeur d'un pointeur est trivial car sa portée est si petite, et son utilisation incorrecte est vérifiée et bien définie. Si votre jeu d'outils et vos ingénieurs ne peuvent pas suivre le programme après une lecture rapide ou s'il existe une vérification d'erreur inappropriée ou un flux de programme incohérent/indulgent, vous avez d'autres problèmes plus importants.

Enfin, votre compilateur et votre environnement ont probablement des protections pour les moments où vous souhaitez introduire des erreurs (gribouillage), détecter les accès à la mémoire libérée et intercepter d'autres UB connexes. Vous pouvez également introduire des diagnostics similaires dans vos programmes, souvent sans affecter les programmes existants.

2
justin

"Il y a des moments où c'est une bonne chose à faire, et des moments où c'est inutile et peut cacher des erreurs"

Je peux voir deux problèmes: Ce code simple:

delete myObj;
myobj = 0

devient un for-liner dans un environnement multithread:

lock(myObjMutex); 
delete myObj;
myobj = 0
unlock(myObjMutex);

La "meilleure pratique" de Don Neufeld ne s'applique pas toujours. Par exemple. dans un projet automobile, nous devions mettre les pointeurs à 0, même dans les destructeurs. J'imagine que, dans les logiciels critiques pour la sécurité, de telles règles ne sont pas rares. Il est plus facile (et sage) de les suivre que d'essayer de persuader l'équipe/le vérificateur de code pour chaque pointeur utilisé dans le code, qu'une ligne qui annule ce pointeur est redondante.

Un autre danger est de s’appuyer sur cette technique dans le code utilisant des exceptions:

try{  
   delete myObj; //exception in destructor
   myObj=0
}
catch
{
   //myObj=0; <- possibly resource-leak
}

if (myObj)
  // use myObj <--undefined behaviour

Dans un tel code, vous produisez une fuite de ressource et ajustez le problème ou le processus se bloque.

Donc, ces deux problèmes qui me traversent la tête spontanément (Herb Sutter en dirait certainement davantage) me posent toutes les questions du type "Comment éviter d'utiliser des pointeurs intelligents et de faire le travail en toute sécurité avec des pointeurs normaux" comme obsolètes.

1
Valentin Heinitz

Permettez-moi de développer ce que vous avez déjà mis dans votre question.

Voici ce que vous avez mis dans votre question, sous forme de puce:


La définition de pointeurs sur NULL après la suppression n'est pas une bonne pratique universelle en C++. Il y a des moments où:

  • c'est une bonne chose à faire
  • et des moments où il est inutile et peut cacher des erreurs.

Cependant, il y a pas de temps quand c'est mauvais! Vous allez pas introduire plus de bugs en l'annulant explicitement, vous ne ferez pas fuite mémoire, vous ne ferez pas provoquer un comportement indéfini.

Donc, en cas de doute, supprimez-le.

Cela dit, si vous sentez que vous devez annuler explicitement un pointeur, cela ressemble à de la division d’une méthode et vous devriez examiner la méthode de refactoring appelée "Méthode d'extraction" pour la diviser en parties séparées.

Oui.

Le seul "inconvénient" que vous pouvez faire est d’introduire une inefficacité (opération de stockage inutile) dans votre programme - mais cette surcharge sera insignifiante par rapport au coût d’allocation et de libération du bloc de mémoire dans la plupart des cas.

Si vous ne le faites pas, vous aurez de vilains bogues de derefernce pointeur un jour.

J'utilise toujours une macro pour la suppression:

#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }

(et similaire pour un tableau, free (), relâchant des handles)

Vous pouvez également écrire des méthodes "self delete" qui prennent une référence au pointeur du code appelant, de sorte qu'elles forcent le pointeur du code appelant à NULL. Par exemple, pour supprimer un sous-arbre de plusieurs objets:

static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
    if (rootObject == NULL)
        return;

    rootObject->UnlinkFromParent();

    for (int i = 0; i < numChildren)
       DeleteSubtree(rootObject->child[i]);

    delete rootObject;
    rootObject = NULL;
}

modifier

Oui, ces techniques ne respectent pas certaines règles relatives à l'utilisation de macros (et oui, de nos jours, vous pourriez probablement obtenir le même résultat avec des modèles) - mais en utilisant plusieurs années, je jamais accès à la mémoire morte - l’un des plus méchants, des plus difficiles et des plus chronophages pour résoudre les problèmes auxquels vous pouvez faire face. En pratique, pendant de nombreuses années, ils ont efficacement éliminé une classe de bugs de chaque équipe que je leur ai présentée.

Il existe également de nombreuses façons de mettre en œuvre ce qui précède - j'essaie simplement d'illustrer l'idée de forcer les utilisateurs à NULL un pointeur s'ils suppriment un objet, plutôt que de leur fournir un moyen de libérer la mémoire sans NULL le pointeur de l'appelant. .

Bien entendu, l'exemple ci-dessus n'est qu'un pas vers un pointeur automatique. Ce que je n'ai pas suggéré parce que l'OP posait spécifiquement la question de ne pas utiliser de pointeur automatique.

1
Jason Williams

Si le code n'appartient pas à la partie la plus critique de votre application en termes de performances, conservez-le simple et utilisez un shared_ptr:

shared_ptr<Foo> p(new Foo);
//No more need to call delete

Il effectue le comptage des références et est thread-safe. Vous pouvez le trouver dans tr1 (espace de noms std :: tr1, #include <memory>) ou, si votre compilateur ne le fournit pas, obtenez-le à partir de boost.

0
beta4

Il y a toujours pointeurs en suspens à s'inquiéter.

0
GrayWizardx

Si vous souhaitez réaffecter le pointeur avant de l'utiliser à nouveau (le déréférencer, le transférer à une fonction, etc.), le pointeur NULL n'est qu'une opération supplémentaire. Toutefois, si vous ne savez pas s'il sera réaffecté avant de l'utiliser à nouveau, définissez-le sur NULL est une bonne idée.

Comme beaucoup l'ont dit, il est bien sûr beaucoup plus simple d'utiliser des pointeurs intelligents.

Edit: Comme Thomas Matthews l’a dit dans cette réponse précédente , si un pointeur est supprimé dans un destructeur, il n’est pas nécessaire de lui attribuer la valeur NULL car il ne sera plus utilisé car l’objet est être déjà détruit.

0
Dustin

Je peux imaginer définir un pointeur sur NULL après l'avoir supprimé, ce qui est utile dans les rares cas où il existe un scénario légitime de réutilisation dans une seule fonction (ou objet). Autrement, cela n'a aucun sens - un pointeur doit pointer sur quelque chose de significatif tant qu'il existe - un point.

0
Nemanja Trifunovic