web-dev-qa-db-fra.com

Est-ce un bon moyen de libérer de la mémoire?

La fonction permettant de libérer une instance de struct Foo est donnée ci-dessous: 

void DestroyFoo(Foo* foo)
{
    if (foo) free(foo);
}

Un de mes collègues a suggéré ce qui suit:

void DestroyFoo(Foo** foo)
{
    if (!(*foo)) return;
    Foo *tmpFoo = *foo;
    *foo = NULL; // prevents future concurrency problems
    memset(tmpFoo, 0, sizeof(Foo));  // problems show up immediately if referred to free memory
    free(tmpFoo);
}

Je vois que définir le pointeur sur NULL après la libération est préférable, mais je ne suis pas sûr des éléments suivants:

  1. Avons-nous vraiment besoin d'assigner le pointeur à un pointeur temporaire? Cela aide-t-il en termes de concurrence et de mémoire partagée?

  2. Est-ce vraiment une bonne idée de mettre le bloc entier à 0 pour forcer le programme à planter ou au moins pour produire des résultats avec une différence significative?

55
Ferit Buyukkececi

Avons-nous vraiment besoin d'assigner le pointeur à un pointeur temporaire? Est-ce que aide en termes de simultanéité et de mémoire partagée?

Il n'a rien à voir avec la concurrence ou la mémoire partagée. C'est inutile.

Est-ce vraiment une bonne idée de mettre le bloc entier à 0 pour forcer le programme à planter ou au moins à produire des résultats avec une valeur significative contradiction?

Non pas du tout.

La solution proposée par votre collègue est terrible. Voici pourquoi:

  • Définir le bloc entier à 0 n'apporte rien non plus. Du fait que quelqu'un utilise accidentellement un bloc free (), il ne le saurait pas en fonction des valeurs du bloc. C'est le genre de bloc que calloc() renvoie . Il est donc impossible de savoir s'il s'agit de mémoire fraîchement allouée (calloc() ou malloc()+memset()) ou de celle qui a été libérée précédemment par votre code. Si quelque chose que ce soit est un travail supplémentaire pour votre programme de mettre à zéro chaque bloc de mémoire qui est libre ().

  • free(NULL); est bien défini et est un no-op, donc la condition if dans if(ptr) {free(ptr);} n'aboutit à rien.

  • Étant donné que free(NULL); est no-op, définir le pointeur sur NULL masquerait ce bogue, car si une fonction appelle réellement free() sur un pointeur déjà libre (), alors ils ne le sauraient pas.

  • la plupart des fonctions utilisateur ont une vérification NULL au début et ne peuvent pas envisager de lui passer NULL comme condition d'erreur: 

void do_some_work(void *ptr) {
    if (!ptr) {
        return; 
    }

   /*Do something with ptr here */
}

Ainsi, toutes ces vérifications supplémentaires et cette remise à zéro donnent un faux sentiment de "robustesse" sans que cela améliore vraiment quoi que ce soit. Il vient de remplacer un problème par un autre, le coût supplémentaire de la performance et du fardeau du code.

Donc, appeler free(ptr); sans fonction d'encapsuleur est à la fois simple et robuste (la plupart des implémentations de malloc() planteraient immédiatement en double libre, ce qui est un bon chose).

Il n’existe pas de moyen facile de contourner "accidentellement" l'appel de free() deux fois ou plus. Il est de la responsabilité du programmeur de garder une trace de toute la mémoire allouée et de free() de manière appropriée. Si quelqu'un trouve cela difficile à gérer, alors C n'est probablement pas le bon langage pour eux.

67
P.P.

Ce que votre collègue suggère de rendre le code "plus sûr" si la fonction est appelée deux fois (voir le commentaire de sleske ... car le mot "plus sûr" peut ne pas signifier la même chose pour tout le monde ... ;-).

Avec votre code, cela plantera très probablement:

Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
DestroyFoo(foo); // will call free on memory already freed

Avec la version du code de vos collègues, cela ne plantera pas:

Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(&foo);
DestroyFoo(&foo); // will have no effect

Désormais, pour ce scénario spécifique, exécuter tmpFoo = 0; (dans DestroyFoo) est suffisant. memset(tmpFoo, 0, sizeof(Foo)); empêchera le crash si Foo a des attributs supplémentaires qui pourraient être mal accédés après la libération de la mémoire.

Donc, je dirais que oui, c'est peut-être une bonne pratique de le faire .... mais ce n'est qu'une sorte de sécurité contre les développeurs qui ont de mauvaises pratiques (car il n'y a aucune raison d'appeler DestroyFoo deux fois sans la réaffecter) ... à la fin, vous rendez DestroyFoo "plus sûr" mais plus lentement (il fait plus de choses pour éviter un mauvais usage). 

9
jpo38

La deuxième solution semble être trop élaborée. Bien sûr, dans certaines situations, cela peut être plus sûr mais les frais généraux et la complexité sont tout simplement trop importants.

Ce que vous devriez faire si vous voulez être sûr est de placer le pointeur sur NULL après avoir libéré de la mémoire. C'est toujours une bonne pratique. 

Foo* foo = malloc( sizeof(Foo) );
DestroyFoo(foo);
foo = NULL;

De plus, je ne sais pas pourquoi les gens vérifient si le pointeur est NULL avant d'appeler free (). Ce n'est pas nécessaire car free () fera le travail pour vous.

Définir la mémoire sur 0 (ou quelque chose d'autre) n'est que dans certains cas une bonne pratique, car free () n'effacera pas la mémoire. Cela marquera simplement qu'une région de la mémoire est libre afin qu'elle puisse être réutilisée. Si vous souhaitez effacer la mémoire afin que personne ne puisse la lire, vous devez la nettoyer manuellement. Mais c'est une opération assez lourde et c'est pourquoi cela ne devrait pas être utilisé pour libérer toute la mémoire. Dans la plupart des cas, libérer sans effacer est suffisant et vous n'avez pas à sacrifier les performances pour effectuer des opérations inutiles.

4
codewarrior
void destroyFoo(Foo** foo)
{
    if (!(*foo)) return;
    Foo *tmpFoo = *foo;
    *foo = NULL;
    memset(tmpFoo, 0, sizeof(Foo));
    free(tmpFoo);
}

Votre code de collègue est mauvais parce que

  • il va planter si foo est NULL
  • il n'y a aucun intérêt à créer une variable supplémentaire
  • il n'y a aucun intérêt à définir les valeurs sur zéros
  • libérer directement une structure ne fonctionne pas s'il contient des choses qui doivent être libérées

Je pense que votre collègue pourrait avoir à l’esprit ce cas d’utilisation

Foo* a = NULL;
Foo* b = createFoo();

destroyFoo(NULL);
destroyFoo(&a);
destroyFoo(&b);

Dans ce cas, ça devrait être comme ça. essayez ici

void destroyFoo(Foo** foo)
{
    if (!foo || !(*foo)) return;
    free(*foo);
    *foo = NULL;
}

Nous devons d’abord regarder Foo, supposons que cela ressemble à ceci

struct Foo
{
    // variables
    int number;
    char character;

    // array of float
    int arrSize;
    float* arr;

    // pointer to another instance
    Foo* myTwin;
};

Maintenant, pour définir comment il devrait être détruit, définissons d'abord comment il devrait être créé

Foo* createFoo (int arrSize, Foo* twin)
{
    Foo* t = (Foo*) malloc(sizeof(Foo));

    // initialize with default values
    t->number = 1;
    t->character = '1';

    // initialize the array
    t->arrSize = (arrSize>0?arrSize:10);
    t->arr = (float*) malloc(sizeof(float) * t->arrSize);

    // a Foo is a twin with only one other Foo
    t->myTwin = twin;
    if(twin) twin->myTwin = t;

    return t;
}

Maintenant, nous pouvons écrire une fonction destroy opposée à la fonction create

Foo* destroyFoo (Foo* foo)
{
    if (foo)
    {
        // we allocated the array, so we have to free it
        free(t->arr);

        // to avoid broken pointer, we need to nullify the twin pointer
        if(t->myTwin) t->myTwin->myTwin = NULL;
    }

    free(foo);

    return NULL;
}

Testez essayez ici

int main ()
{
    Foo* a = createFoo (2, NULL);
    Foo* b = createFoo (4, a);

    a = destroyFoo(a);
    b = destroyFoo(b);

    printf("success");
    return 0;
}
1
Khaled.K

Malheureusement, cette idée ne fonctionne tout simplement pas.

Si l’intention était de doubler la franchise, cela ne couvre pas les cas suivants.

Supposons ce code:

Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
free (ptr_1);
free (ptr_2); /* This is a bug */

La proposition est d'écrire à la place:

Foo *ptr_1 = (FOO*) malloc(sizeof(Foo));
Foo *ptr_2 = ptr_1;
DestroyFoo (&ptr_1);
DestroyFoo (&ptr_2); /* This is still a bug */

Le problème est que le deuxième appel à DestroyFoo() va toujours planter, car ptr_2 n'est pas réinitialisé sur NULL et pointe toujours vers la mémoire déjà libérée.

0
Marc Alff