¿Es esta una buena manera de liberar memoria en C?


La función para liberar una instancia de struct Foo se da a continuación:

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

Un colega mío sugirió lo siguiente en su lugar:

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);
}

Veo que establecer el puntero a NULL después de liberar es mejor, pero no estoy seguro de lo siguiente:

  1. ¿Realmente necesitamos asignar el puntero a uno temporal? ¿Ayuda en términos de concurrencia y memoria compartida?
  2. ¿Es realmente una buena idea establecer todo el bloque en 0 para forzar la ¿el programa se bloqueará o al menos producirá resultados con una discrepancia significativa?

Gracias de antemano!

Author: Ajedi32, 2016-01-07

5 answers

¿Realmente necesitamos asignar el puntero a uno temporal? Haciéndolo ayuda en términos de concurrencia y memoria compartida?

No tiene nada que ver con la concurrencia o la memoria compartida. No tiene sentido.

¿Es realmente una buena idea establecer todo el bloque en 0 para forzar la programa de bloqueo o al menos a la salida de resultados con significativa discrepancia?

No. Para nada.

La solución sugerida por su colega es terrible. He aquí por qué:

  • Establecer el bloque entero a 0 tampoco logra nada. Debido a que alguien está usando un bloque ed de free () accidentalmente, no lo sabrían basándose en los valores del bloque. Ese es el tipo de bloque calloc() devuelve. Así que es imposible saber si se trata de la memoria recién asignada (calloc() o malloc()+memset()) o la que ha sido libre()'ed por su código anterior. Si algo es trabajo extra para su programa para poner a cero cada bloque de memoria que está siendo free () ' ed.

  • free(NULL); está bien definido y es un no-op, por lo que la condición if en if(ptr) {free(ptr);} no logra nada.

  • Dado que free(NULL); no es op, establecer el puntero a NULL en realidad ocultaría ese error, porque si alguna función realmente está llamando a free() en un puntero ya free()'ed, entonces no lo sabrían.

  • La mayoría de las funciones de usuario tendrían una comprobación NULA al inicio y podrían no considerar pasarle NULL como condición de error:

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

   /*Do something with ptr here */
}

Así que todas esas comprobaciones adicionales y cero ' ing hacia fuera da una falsa sensación de "robustez", mientras que en realidad no mejoró nada. Simplemente reemplazó un problema con otro el costo adicional de rendimiento y código hinchado.

Así que simplemente llamar a free(ptr); sin ninguna función de envoltura es simple y robusto (la mayoría de las implementaciones malloc()se bloquearían inmediatamente en double free, que es una buena cosa).

No hay manera fácil de evitarlo "accidentalmente" llamando free() dos veces o más. Es responsabilidad del programador mantener un registro de toda la memoria asignada y free() apropiadamente. Si alguien encuentra esto difícil de manejar, entonces C probablemente no es el lenguaje adecuado para ellos.

 66
Author: P.P.,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-11-11 11:26:07

Lo que su collegue sugiere hará que el código sea "más seguro" en caso de que la función se llame dos veces (ver sleske comment...as "más seguro" puede no significar lo mismo para todos...;-).

Con su código, lo más probable es que esto se bloquee:

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

Con la versión del código de sus colegas, esto no se bloqueará:

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

Ahora, para este escenario específico, hacer tmpFoo = 0; (dentro de DestroyFoo) es suficiente. memset(tmpFoo, 0, sizeof(Foo)); evitará el bloqueo si Foo tiene atributos adicionales a los que se puede acceder erróneamente después de la memoria es liberado.

Así que diría que sí, puede ser una buena práctica hacerlo....pero es solo una especie de seguridad contra desarrolladores que tienen malas prácticas (porque definitivamente no hay razón para llamar DestroyFoo dos veces sin reasignarlo) {al final, haces DestroyFoo "más seguro" pero más lento (hace más cosas para evitar un mal uso de él).

 9
Author: jpo38,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-07 11:31:48

La segunda solución parece estar sobre diseñada. Por supuesto, en alguna situación podría ser más seguro, pero la sobrecarga y la complejidad es demasiado grande.

Lo que debe hacer si desea estar en un lado seguro es establecer el puntero a NULL después de liberar memoria. Siempre es una buena práctica.

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

Además, no se por qué la gente está comprobando si el puntero es NULL antes de llamar a free(). Esto no es necesario ya que free() hará el trabajo por ti.

Configuración de la memoria a 0 (o algo más) es solo en algunos casos una buena práctica ya que free () no borrará la memoria. Solo marcará una región de la memoria para ser libre para que pueda ser reutilizada. Si desea borrar la memoria, para que nadie pueda leerla, debe limpiarla manualmente. Pero esta es una operación bastante pesada y es por eso que no se debe usar para liberar toda la memoria. En la mayoría de los casos, liberar sin limpiar es suficiente y no tiene que sacrificar el rendimiento para hacer algo innecesario operación.

 4
Author: codewarrior,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-07 10:22:30
void destroyFoo(Foo** foo)
{
    if (!(*foo)) return;
    Foo *tmpFoo = *foo;
    *foo = NULL;
    memset(tmpFoo, 0, sizeof(Foo));
    free(tmpFoo);
}

Su código de colega es malo porque

  • , se bloqueará si foo es NULL
  • no tiene sentido crear una variable adicional
  • no tiene sentido establecer los valores en ceros
  • liberar una estructura directamente no funciona si contiene cosas que deben ser liberadas{[14]]}

Creo que lo que su colega podría tener en mente es este caso de uso

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

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

En ese caso, debería ser así. pruebe aquí

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

Primero tenemos que echar un vistazo a Foo, supongamos que se ve así

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

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

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

Ahora para definir cómo debe ser destruido, primero definamos cómo debe ser creado

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;
}

Ahora podemos escribir una función destroy oppose a la función 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;
}

Prueba prueba aquí

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

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

    printf("success");
    return 0;
}
 1
Author: Khaled.K,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-01-15 13:06:25

Desafortunadamente, esta idea no está funcionando.

Si la intención era atrapar double free, no está cubriendo casos como los siguientes.

Asume este código:

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

La propuesta es escribir en su lugar:

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

El problema es que la segunda llamada a DestroyFoo() todavía accidente, porque ptr_2 no se restablece a NULL, y todavía la memoria ya liberado.

 0
Author: Marc Alff,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2018-09-05 08:27:24