web-dev-qa-db-fra.com

Faut-il vraiment mettre les pointeurs sur `NULL` après les avoir libérés?

Il semble y avoir deux arguments pour lesquels on devrait définir un pointeur sur NULL après les avoir libérés.

Évitez de vous écraser lorsque vous doublez libérer des pointeurs.

Short: L'appel de free() une seconde fois, par accident, ne plante pas lorsqu'il est défini sur NULL.

  • Presque toujours, cela masque un bogue logique car il n'y a aucune raison d'appeler free() une seconde fois. Il est plus sûr de laisser l'application planter et de pouvoir la réparer.

  • Il n'est pas garanti que la mémoire plante tombe en panne, car une nouvelle mémoire est parfois allouée à la même adresse.

  • La double libération survient surtout lorsqu'il y a deux pointeurs pointant vers la même adresse.

Les erreurs logiques peuvent également entraîner une corruption des données.

Évitez de réutiliser les pointeurs libérés

Short: l'accès aux pointeurs libérés peut entraîner une corruption des données si malloc() alloue de la mémoire au même endroit, sauf si le pointeur libéré est défini sur NULL

  • Rien ne garantit que le programme se bloque lors de l'accès au pointeur NULL, si le décalage est suffisamment important (someStruct->lastMember, theArray[someBigNumber]). Au lieu de planter, les données seront corrompues.

  • Définir le pointeur sur NULL ne peut pas résoudre le problème d'avoir un pointeur différent avec la même valeur.

Questions

Voici un article contre la définition aveugle d'un pointeur sur NULL après avoir libéré .

  • Lequel est le plus difficile à déboguer?
  • Est-il possible d'attraper les deux?
  • Quelle est la probabilité que de tels bogues entraînent la corruption des données au lieu de planter?

N'hésitez pas à développer cette question.

48
Georg Schölly

Je ne fais pas ça. Je ne me souviens pas particulièrement des bugs qu'il aurait été plus facile de traiter si je l'avais fait. Mais cela dépend vraiment de la façon dont vous écrivez votre code. Il y a environ trois situations où je libère quelque chose:

  • Lorsque le pointeur qui le tient est sur le point de sortir de la portée ou fait partie d'un objet sur le point de l'être ou d'être libéré.
  • Quand je remplace l'objet par un nouvel objet (comme avec la réaffectation, par exemple).
  • Quand je libère un objet qui est éventuellement présent.

Dans le troisième cas, vous définissez le pointeur sur NULL. Ce n’est pas précisément parce que vous le libérez, mais parce que quoi que ce soit soit optionnel. NULL est donc une valeur spéciale signifiant "je n’en ai pas".

Dans les deux premiers cas, définir le pointeur sur NULL me semble être un travail occupé sans but particulier:

int doSomework() {
    char *working_space = malloc(400*1000);
    // lots of work
    free(working_space);
    working_space = NULL; // wtf? In case someone has a reference to my stack?
    return result;
}

int doSomework2() {
    char * const working_space = malloc(400*1000);
    // lots of work
    free(working_space);
    working_space = NULL; // doesn't even compile, bad luck
    return result;
}

void freeTree(node_type *node) {
    for (int i = 0; i < node->numchildren; ++i) {
        freeTree(node->children[i]);
        node->children[i] = NULL; // stop wasting my time with this rubbish
    }
    free(node->children);
    node->children = NULL; // who even still has a pointer to node?

    // Should we do node->numchildren = 0 too, to keep
    // our non-existent struct in a consistent state?
    // After all, numchildren could be big enough
    // to make NULL[numchildren-1] dereferencable,
    // in which case we won't get our vital crash.

    // But if we do set numchildren = 0, then we won't
    // catch people iterating over our children after we're freed,
    // because they won't ever dereference children.

    // Apparently we're doomed. Maybe we should just not use
    // objects after they're freed? Seems extreme!
    free(node);
}

int replace(type **thing, size_t size) {
    type *newthing = copyAndExpand(*thing, size);
    if (newthing == NULL) return -1;
    free(*thing);
    *thing = NULL; // seriously? Always NULL after freeing?
    *thing = newthing;
    return 0;
}

Il est vrai que NULL-ing le pointeur peut le rendre plus évident si vous avez un bug où vous essayez de le déréférencer après avoir libéré. Le déréférencement ne fait probablement pas de mal immédiat si vous ne supprimez pas le pointeur, mais est inacceptable à long terme.

Il est également vrai que NULL-ing le pointeur obscurcit bugs où vous double-libre. Le second free ne fait pas de mal immédiat si vous faites NULL le pointeur, mais est faux à long terme (car il trahit le fait que vos cycles de vie d'objet sont brisés). Vous pouvez affirmer que les éléments sont non nuls lorsque vous les libérez, mais le code suivant libère une structure qui contient une valeur facultative:

if (thing->cached != NULL) {
    assert(thing->cached != NULL);
    free(thing->cached);
    thing->cached = NULL;
}
free(thing);

Ce que ce code vous dit, c’est que vous en avez trop. CA devrait etre:

free(thing->cached);
free(thing);

Je dis, NULL le pointeur si c'est supposé rester utilisable. S'il n'est plus utilisable, évitez de le faire paraître faussement en ajoutant une valeur potentiellement significative, telle que NULL. Si vous voulez provoquer une erreur de page, utilisez une valeur dépendant de la plate-forme, qui ne peut pas être annulée, mais que le reste de votre code ne traitera pas comme une valeur spéciale "Tout va bien et tout ce qui est beau":

free(thing->cached);
thing->cached = (void*)(0xFEFEFEFE);

Si vous ne trouvez pas une telle constante sur votre système, vous pourrez peut-être attribuer une page illisible et/ou non inscriptible et en utiliser l'adresse.

7
Steve Jessop

Si vous ne définissez pas le pointeur sur NULL, il existe une chance non négligeable que votre application continue de s'exécuter dans un état non défini et se bloque ultérieurement à un point totalement indépendant. Ensuite, vous passerez beaucoup de temps à déboguer une erreur inexistante avant de savoir qu'il s'agit d'une corruption de mémoire antérieure.

Je mettrais le pointeur sur NULL car il y a plus de chances que vous rencontriez l'erreur plus tôt que si vous ne l'aviez pas définie sur NULL. L’erreur logique de libérer une seconde fois de la mémoire reste à faire et l’erreur selon laquelle votre application ne se bloque PAS sur l’accès null-pointeur avec un décalage suffisant est à mon avis tout à fait académique bien qu’elle ne soit pas impossible.

Conclusion: je préférerais placer le pointeur sur NULL.

3
Kosi2801

La réponse dépend de (1) la taille du projet, (2) la durée de vie attendue de votre code, (3) la taille de l'équipe . Sur un petit projet avec une durée de vie courte, vous pouvez ignorer le réglage des pointeurs sur NULL et déboguer.

Sur un grand projet de longue durée, il existe de bonnes raisons de définir les pointeurs sur NULL: (1) La programmation défensive est toujours bonne. Votre code est peut-être correct, mais le débutant d'à côté risque toujours de se débattre avec les pointeurs (2) Ma conviction personnelle est que toutes les variables ne doivent contenir que des valeurs valides à tout moment. Après une suppression/libération, le pointeur n'est plus une valeur valide, il doit donc être supprimé de cette variable. Le remplacer par NULL (la seule valeur de pointeur toujours valide) est une bonne étape . (3) Le code ne meurt jamais. Il est toujours réutilisé, et souvent d'une manière que vous n'aviez pas imaginée au moment où vous l'avez écrit. Votre segment de code pourrait être compilé dans un contexte C++ et probablement déplacé vers un destructeur ou une méthode appelée par un destructeur. Les interactions des méthodes et des objets virtuels en cours de destruction sont des pièges subtils, même pour les programmeurs très expérimentés . (4) Si votre code finit par être utilisé dans un contexte multithread, un autre thread peut lire ceci: variable et essayer d'y accéder. De tels contextes surviennent souvent lorsque le code hérité est encapsulé et réutilisé dans un serveur Web. Ainsi, un moyen encore plus efficace de libérer de la mémoire (d'un point de vue paranoïaque) consiste à (1) copier le pointeur sur une variable locale, (2) définir la variable d'origine sur NULL, (3) supprimer/libérer la variable locale. 

3
Carsten Kuckuk

Si le pointeur doit être réutilisé, il doit être redéfini sur 0(NULL) après utilisation, même si l'objet qu'il pointait n'était pas libéré du tas. Cela permet une vérification valide contre NULL comme si (p) {// fait quelque chose}. De même, le fait de libérer un objet dont l'adresse est pointée par le pointeur ne signifie pas que le pointeur est défini sur 0 après l'appel du mot-clé delete ou de la fonction free. 

Si le pointeur est utilisé une fois et qu'il fait partie d'une portée qui le rend local, il n'est pas nécessaire de le définir sur NULL car il sera supprimé de la pile après le retour de la fonction.

Si le pointeur est un membre (struct ou classe), vous devez le définir sur NULL après avoir libéré le ou les objets sur un pointeur double pour vérification valide par rapport à NULL. 

Cela vous aidera à atténuer les maux de tête causés par les pointeurs non valides tels que '0xcdcd ...', etc. Donc, si le pointeur est 0, alors vous savez qu'il ne pointe pas vers une adresse et pouvez vous assurer que l'objet est libéré du tas.

2
bvrwoo_3376

Les deux sont très importants car ils traitent avec un comportement indéfini. Vous ne devez laisser aucun moyen d'adopter un comportement indéfini dans votre programme. Les deux peuvent conduire à des plantages, à la corruption des données, à des bugs subtils et à d’autres conséquences néfastes.

Les deux sont assez difficiles à déboguer. Les deux ne peuvent pas être évités à coup sûr, surtout dans le cas de structures de données complexes. Quoi qu'il en soit, vous êtes beaucoup mieux si vous suivez les règles suivantes:

  • toujours initialiser les pointeurs - définissez-les sur NULL ou sur une adresse valide
  • après avoir appelé free (), positionnez le pointeur sur NULL
  • vérifiez tous les pointeurs pouvant être NULL pour être NULL avant de les déréférencer.
1
sharptooth

En C++, vous pouvez attraper à la fois en implémentant votre propre pointeur intelligent (ou en dérivant d'implémentations existantes) et en implémentant quelque chose comme

void release() {
    assert(m_pt!=NULL);
    T* pt = m_pt;
    m_pt = NULL;
    free(pt);
}

T* operator->() {
    assert(m_pt!=NULL);
    return m_pt;
}

Alternativement, en C, vous pouvez au moins fournir deux macros ayant le même effet:

#define SAFE_FREE(pt) \
    assert(pt!=NULL); \
    free(pt); \
    pt = NULL;

#define SAFE_PTR(pt) assert(pt!=NULL); pt
1
Sebastian

Ces problèmes ne sont le plus souvent que des symptômes d'un problème beaucoup plus profond. Cela peut se produire pour toutes les ressources nécessitant une acquisition et une version ultérieure, par exemple. mémoire, fichiers, bases de données, connexions réseau, etc. Le problème essentiel est que vous avez perdu la trace des allocations de ressources en raison d'une structure de code manquante, en lançant des mallocs aléatoires et en libérant tout le code.

Organisez le code autour de DRY - ne vous répétez pas. Gardez les choses liées ensemble. Faites une chose seulement et faites-le bien. Le "module" qui alloue une ressource est responsable de sa libération et doit fournir une fonction permettant de garder les pointeurs également. Pour chaque ressource spécifique, vous avez alors exactement un endroit où elle est allouée et un endroit où elle est libérée, les deux proches les uns des autres.

Supposons que vous souhaitiez diviser une chaîne en sous-chaînes. Directement à l’aide de malloc (), votre fonction doit tout gérer: analyser la chaîne, allouer la bonne quantité de mémoire, copier les sous-chaînes qui y figurent, et et. Rendez la fonction assez compliquée et la question n'est pas de savoir si vous allez perdre la trace des ressources, mais quand.

Votre premier module prend en charge l'allocation de mémoire réelle:


    void *MemoryAlloc (size_t size)
    void  MemoryFree (void *ptr)

Il n’ya qu’un seul endroit dans votre base de code où sont appelées malloc () et free ().

Ensuite, nous devons allouer des chaînes:


    StringAlloc (char **str, size_t len)
    StringFree (char **str)

Ils veillent à ce que len + 1 soit nécessaire et que le pointeur soit défini sur NULL lorsqu'il est libéré. Fournissez une autre fonction pour copier une sous-chaîne:


    StringCopyPart (char **dst, const char *src, size_t index, size_t len)

Il prendra soin si index et len ​​sont dans la chaîne src et la modifiera si nécessaire. Il appellera StringAlloc pour dst, et il veillera à ce que dst soit correctement terminé.

Vous pouvez maintenant écrire votre fonction split. Vous n'avez plus à vous soucier des détails de bas niveau, vous devez simplement analyser la chaîne et en extraire les sous-chaînes. La majeure partie de la logique est maintenant dans le module auquel elle appartient, au lieu d'être mélangée dans une grande monstruosité.

Bien sûr, cette solution a ses propres problèmes. Il fournit des couches d'abstraction et chaque couche, tout en résolvant d'autres problèmes, vient avec son propre ensemble.

1
Secure

Il n’ya pas vraiment de partie "plus importante" à l’un des problèmes que vous essayez d’éviter. Vous devez vraiment, vraiment éviter les deux si vous voulez écrire un logiciel fiable. Il est également très probable que l’un ou l’autre des éléments ci-dessus entraîne une corruption des données, l’utilisation de votre serveur Web et d’autres activités amusantes dans ce sens.

Il faut aussi garder à l’esprit une autre étape importante: placer le pointeur sur NULL après la libération n’est que la moitié du travail. Idéalement, si vous utilisez cet idiome, vous devriez également envelopper l'accès du pointeur dans quelque chose comme ceci:

if (ptr)
  memcpy(ptr->stuff, foo, 3);

Si vous positionnez le pointeur lui-même sur NULL, le programme ne fonctionnera que dans des endroits inopportuns, ce qui est probablement mieux que de corrompre des données en mode silencieux, mais ce n'est toujours pas ce que vous voulez.

0
Timo Geusch

Rien ne garantit que le programme se bloque lors de l'accès au pointeur NULL.

Peut-être pas par la norme, mais vous auriez du mal à trouver une implémentation qui ne la définisse pas comme une opération illégale qui provoque un blocage ou une exception (selon l'environnement d'exécution).

0
Anon.