Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Regla 0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes

Memoria, punteros y tipos (0x30XX)

Universidad Nacional de Río Negro

0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes

Enunciado normativo

Por cada asignación dinámica DEBE existir exactamente una liberación con free, y DEBE asignarse NULL al puntero inmediatamente después de liberarlo. NO DEBE liberarse memoria que no provino del asignador.

¿Por qué existe esta regla?

El problema

free(ptr) devuelve el bloque al heap, pero no modifica la variable ptr, que queda apuntando a memoria cuya vida terminó: un dangling pointer. Toda lectura o escritura posterior es comportamiento indefinido (C11 §6.5.3.2), porque el bloque pudo reutilizarse. Anular el puntero convierte el uso posterior en un fallo determinista y fácil de rastrear: free(NULL) es un no-op válido.

Consecuencias de violarla

Tipo de consecuenciaEfecto concreto
Comportamiento indefinidoUse-after-free sobre el bloque liberado.
Doble liberaciónEl segundo free corrompe las estructuras del asignador y aborta.
Fuga de memoriaNo liberar deja bloques inalcanzables hasta el fin del proceso.

Fundamento en el estándar y en la cátedra

C11 §7.22.3.3 establece que sólo puede liberarse un puntero devuelto por las funciones de asignación (o NULL). La cátedra adopta el par “liberar y anular” como una unidad: no se piensa en una sin la otra. Es la contraparte simétrica de 0x3001h: Siempre verificá la asignación exitosa de memoria dinámica.

Alcance y excepciones

Aplica a todo recurso obtenido con malloc, calloc, realloc o strdup. No se debe llamar a free sobre variables estáticas ni automáticas, ni sobre punteros interiores (&arreglo[3]).

Ejemplos exhaustivos

❌ Contraejemplo 1 — Uso después de liberar

char *mensaje = malloc(64);
copiar(mensaje, "hola");
free(mensaje);
printf("%s\n", mensaje);

Por qué falla: el printf lee un bloque que ya no pertenece al programa; si se reutilizó, la salida es basura impredecible. Anular mensaje habría hecho que el acceso falle de forma visible.

❌ Contraejemplo 2 — Liberar dirección de pila o interior de un bloque

int datos[10];
free(datos);

Por qué falla: datos es un arreglo automático en la pila, no un bloque del heap. Pasar su dirección a free es comportamiento indefinido y, en glibc, aborta con free(): invalid pointer. El mismo error ocurre con free(&datos[3]), porque no es la dirección que devolvió malloc.

✅ Ejemplo conforme 1 — Liberar y anular

char *mensaje = malloc(64);
copiar(mensaje, "hola");
free(mensaje);
mensaje = NULL;

La anulación cuesta una línea y elimina toda una clase de errores posteriores: documenta que el recurso ya no está disponible.

✅ Ejemplo conforme 2 — Constructor y destructor simétricos del TAD

lista_t *lista_crear(void)
{
    lista_t *lista = malloc(sizeof(*lista));
    if (lista == NULL)
    {
        return NULL;
    }
    lista->cabeza = NULL;
    lista->cantidad = 0;
    return lista;
}
void lista_destruir(lista_t *lista)
{
    if (lista == NULL)
    {
        return;
    }
    liberar_nodos(lista->cabeza);
    free(lista);
}

El módulo encapsula el ciclo de vida completo: quien crea con lista_crear destruye con lista_destruir, y el cliente anula su variable. Esta simetría es la que exige 0x4004h: Mantené la simetría de recursos al abrir y cerrar archivos en el mismo nivel de abstracción para archivos.

⚠️ Casos límite

Cómo detectarla

HerramientaComandoSeñal
gaffgaff check archivo.cfree(ptr) sin ptr = NULL inmediato; reserva sin free asociado.
gcc / clanggcc -Wall -Wextra -std=c11 -fanalyzer ...use after 'free' en caminos simples.
valgrindvalgrind --leak-check=full ./programadefinitely lost, invalid free, use of uninitialised value.

Checklist de autocontrol

Reglas relacionadas

Antipatrón: Puntero colgante sin asignar NULL tras free()

Síntoma en el código del estudiante

La variable se libera, pero conserva la dirección anterior. Después del free siguen apareciendo usos del mismo identificador:

free(ptr);
/* ... más instrucciones ... */
ptr->dato = 5;   /* use-after-free */
free(ptr);       /* double free */

El síntoma clásico es no ver nunca un ptr = NULL; acompañando a la liberación.

Diagnóstico

Mecanismo del defecto

free(ptr) devuelve el bloque al asignador, pero no modifica la variable: el parámetro se recibe por valor, así que la copia local de free se descarta y ptr sigue conteniendo la misma dirección. Esa dirección ahora apunta a memoria que el asignador considera libre.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
void liberar(nodo_t *n)
{
    free(n);
}
✅ Código refactorizado
void liberar(nodo_t **n)
{
    if (*n != NULL) {
        free(*n);
        *n = NULL;
    }
}

Anular el puntero del llamador requiere pasar su dirección; si se pasa por valor, la anulación queda en una copia local y no sirve.

❌ Contraejemplo adicional — doble liberación indirecta
free(a);
free(b);   /* b era un alias de a: double free */
✅ Refactorización con anulación simétrica
free(a);
if (b == a) {
    b = NULL;
}
a = NULL;
⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ ./programa
free(): double free detected in tcache 2
Aborted (core dumped)

# Con AddressSanitizer:
ERROR: AddressSanitizer: heap-use-after-free on address 0x...
READ of size 4 at 0x... thread T0

Checklist de verificación

Reglas relacionadas

Antipatrón: Retorno de puntero a variable local (Dangling Stack Pointer)

Síntoma en el código del estudiante

Una función declara una variable (o un arreglo) local y retorna su dirección:

int *elegir(void)
{
    int local = 42;
    return &local;
}

El síntoma es el & sobre un identificador cuyo alcance termina al cerrar la función. El error aparece incluso en funciones que “parecen” andar.

Diagnóstico

Mecanismo del defecto

Las variables automáticas viven en el marco de pila (stack frame) de la función. Ese marco se crea al entrar y se destruye al retornar: el puntero de pila se restaura y el espacio queda disponible para el siguiente llamado.

El return &local entrega una dirección que ya no pertenece a ningún objeto vivo. Al desreferenciarla desde el llamador, cualquier función intermedia puede haber reutilizado esos bytes. El valor que se lee es basura; el valor que se escribe pisa el marco de otra función.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
char *nombre(void)
{
    char buffer[32] = "apunte";
    return buffer;
}
✅ Código refactorizado — reserva en el heap
char *nombre(void)
{
    char *buffer = malloc(32 * sizeof(*buffer));
    if (buffer == NULL) {
        return NULL;
    }
    strcpy(buffer, "apunte");
    return buffer;
}

El llamador se vuelve dueño del bloque y debe liberarlo (regla 0x3006h).

✅ Alternativa — buffer provisto por el llamador
void nombre(char *destino, size_t capacidad)
{
    snprintf(destino, capacidad, "%s", "apunte");
}

Esta variante evita la reserva y deja la propiedad en el llamador.

⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ gcc -Wall -Wextra -std=c11 programa.c
programa.c: In function 'nombre':
programa.c:4:12: warning: function returns address of local variable [-Wreturn-local-addr]
    4 |     return buffer;

$ ./programa
Segmentation fault (core dumped)

Checklist de verificación

Reglas relacionadas

Antipatrón: Invocación a free() sobre memoria estática o variables automáticas de pila

Síntoma en el código del estudiante

Se llama a free con la dirección de una variable común o de un arreglo estático:

int x = 42;
free(&x);

o bien:

static char nombre[32] = "apunte";
free(nombre);

El puntero no proviene de malloc, calloc ni realloc.

Diagnóstico

Mecanismo del defecto

free administra exclusivamente bloques entregados por el heap a través de los asignadores dinámicos. La variable x vive en la pila y nombre en el segmento de datos; ninguna está registrada en las estructuras internas del asignador.

Al llamar free(&x), glibc busca los metadatos del bloque antes de la dirección indicada. Como no hay cabecera válida, detecta la inconsistencia y aborta con SIGABRT. Algunos asignadores, o versiones viejas, corrompen sus listas internas y dejan el heap en un estado inconsistente: a partir de ahí, cualquier reserva o liberación posterior falla.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
void procesar(void)
{
    int x = 42;
    free(&x);
}
✅ Código refactorizado — reservar para poder liberar
void procesar(void)
{
    int *p = malloc(sizeof(*p));
    if (p == NULL) {
        return;
    }
    *p = 42;
    free(p);
    p = NULL;
}

Si el dato puede vivir en la pila, simplemente no se libera: sale de alcance solo.

✅ Estructura mixta correcta
struct config {
    char *nombre;
    int valor;
};

void destruir(struct config *c)
{
    free(c->nombre);   /* solo el campo dinámico */
    c->nombre = NULL;
    /* c y c->valor son locales o parte de otro objeto: no se liberan */
}
⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ ./programa
free(): invalid pointer
Aborted (core dumped)

# Con AddressSanitizer:
ERROR: AddressSanitizer: attempting free on address which was not
 malloc()-ed: 0x... in thread T0

Checklist de verificación

Reglas relacionadas

Antipatrón: Asignación de punteros a arreglos locales en parámetros de salida

Síntoma en el código del estudiante

La función “devuelve” un arreglo escribiéndolo en un parámetro de salida, pero el arreglo es local:

void obtener(int **res)
{
    int arr[5] = {0};
    *res = arr;
}

El llamador recibe una dirección que apunta al marco de pila ya destruido.

Diagnóstico

Mecanismo del defecto

arr es una variable automática: vive en el marco de pila de obtener. La asignación *res = arr guarda en el llamador la dirección del primer elemento, pero esa dirección solo es válida mientras obtener esté activa. Al retornar, el puntero de pila se restaura y el marco queda reutilizable.

Igual que en 0x3002h, el puntero resultante es colgante: apunta a memoria que ya no pertenece a ningún objeto vivo. Cualquier función que se llame después puede sobrescribir esos bytes, y la lectura desde el llamador devuelve basura o pisa datos ajenos.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
void obtener(int **res)
{
    int arr[5] = {0};
    *res = arr;
}
✅ Código refactorizado — el llamador provee el buffer
void obtener(int *destino, size_t n)
{
    for (size_t i = 0; i < n; i++) {
        destino[i] = 0;
    }
}

El llamador declara el arreglo y sigue siendo su dueño; no hay puntero colgante.

✅ Alternativa — reserva dinámica con propiedad explícita
int *obtener(size_t n)
{
    int *arr = calloc(n, sizeof(*arr));
    if (arr == NULL) {
        return NULL;
    }
    return arr;   /* el llamador debe free()ar */
}

La memoria del heap vive más allá de la función; la propiedad se documenta (regla 0x3006h).

⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ gcc -Wall -Wextra -std=c11 programa.c
# puede no advertir nada por el paso por ** 

$ ./programa
# datos correctos al principio, basura tras llamar a otra función
0 0 0 0 0
-1073743 32766 0 0 0

Checklist de verificación

Reglas relacionadas

Antipatrón: Modificación directa del puntero base asignado por malloc()

Síntoma en el código del estudiante

El puntero devuelto por malloc se avanza con ++ y luego se pasa a free:

char *p = malloc(100);
while (*p) {
    p++;
}
free(p);

p ya no apunta al inicio del bloque cuando llega al free.

Diagnóstico

Mecanismo del defecto

malloc entrega la dirección de inicio del bloque, que es la única que el asignador reconoce. La aritmética de punteros modifica la variable p: cada p++ la corre un elemento. Al llegar al free, el valor es inicio + desplazamiento.

free exige exactamente el puntero original. Al recibir una dirección desplazada, no encuentra la cabecera del bloque donde la busca, interpreta basura como metadatos y corrompe el heap. El resultado típico es free(): invalid pointer o, peor, corrupción silenciosa detectada mucho después.

Además, la dirección del inicio queda irrecuperable: aunque se quisiera liberar bien, ya no se sabe dónde empezaba el bloque. Es una pérdida de la referencia base.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
char *p = malloc(100);
while (*p != '\0') {
    p++;          /* mueve la base */
}
free(p);          /* dirección desplazada: free inválido */
✅ Código refactorizado — puntero auxiliar
char *p = malloc(100);
if (p == NULL) {
    return -1;
}
char *iter = p;
while (*iter != '\0') {
    iter++;
}
free(p);   /* se libera la base, no el iterador */
p = NULL;

El puntero base p nunca se mueve; el iterador es una copia descartable.

✅ Alternativa — indexación por corchetes
char *p = malloc(100);
if (p == NULL) {
    return -1;
}

size_t i = 0;
while (p[i] != '\0') {
    i++;
}

free(p);
p = NULL;

p[i] equivale a *(p + i) sin modificar p.

⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ ./programa
free(): invalid pointer
Aborted (core dumped)
# Con AddressSanitizer:
ERROR: AddressSanitizer: attempting free on address which was not
 malloc()-ed: 0x... in thread T0

Checklist de verificación

Reglas relacionadas