Regla 0x301Eh: Asigná NULL al puntero tras liberar un recurso opaco en el ámbito del cliente
Memoria, punteros y tipos (0x30XX)
0x301Eh: Asigná NULL al puntero tras liberar un recurso opaco en el ámbito del cliente¶
Enunciado normativo¶
Tras invocar la función destructora de un recurso opaco, DEBE asignarse
NULLal puntero del cliente que lo referenciaba. Si la interfaz ofrece una variante que recibeT **y anula internamente, DEBE preferirse esa variante.
¿Por qué existe esta regla?¶
El problema¶
La función destructora de un TAD opaco libera el recurso, pero recibe el puntero por valor: no puede anular la variable del cliente, que sigue conteniendo la dirección del bloque ya liberado. Ese es el dangling pointer. En un tipo opaco el cliente no puede inspeccionar campos: la única señal de validez es el propio puntero, y por eso hay que anularlo de forma inmediata y visible.
Consecuencias de violarla¶
| Tipo de consecuencia | Efecto concreto |
|---|---|
| Comportamiento indefinido | Volver a usar el puntero desreferencia memoria liberada (C11 §6.5.3.2). |
| Doble liberación | Un segundo lista_destruir(lista) corrompe el heap y aborta con SIGABRT. |
| Bug silencioso | Si el bloque fue reutilizado, el programa lee datos ajenos sin fallar de inmediato. |
Fundamento en el estándar y en la cátedra¶
C11 §7.22.3.3 declara que el valor de un puntero es indeterminado después de
free(); leerlo para compararlo contra NULL no es válido. Anularlo restaura un
valor determinista. Liberar y no anular equivale a dejar una trampa abierta.
Alcance y excepciones¶
Aplica al código cliente que destruye una instancia opaca, y también al
ámbito interno de un módulo que destruye recursos locales. No aplica a
punteros que quedan fuera de alcance inmediatamente después, ni a variables
const que no van a reutilizarse. Tampoco reemplaza la regla general de
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes para memoria dinámica común: acá el foco está en la asimetría del
destructor que no puede anular por sí mismo.
Ejemplos exhaustivos¶
❌ Contraejemplo 1 — Uso posterior al destructor¶
lista_t *lista = lista_crear();
if (lista == NULL)
{
return -1;
}
lista_agregar(lista, 42);
lista_destruir(lista);
printf("Cantidad: %zu\n", lista_cantidad(lista));Por qué falla: la impresión vuelve a pasar la dirección liberada. Si el
asignador reutilizó el bloque, lista_cantidad lee un struct que ya no es una
lista; si no, accede a memoria muerta. El error no ocurre en la línea del
destructor, sino una línea después.
❌ Contraejemplo 2 — Destrucción repetida dentro de un lazo¶
for (size_t i = 0; i < n; i++)
{
procesar(recursos[i]);
lista_destruir(recursos[i]);
}Por qué falla: si recursos[i] reaparece apuntando al mismo bloque (por una
copia previa), la segunda invocación deriva en double free. Anular cada
elemento inmediatamente convierte un error catastrófico en un no-op silencioso,
porque free(NULL) está permitido.
✅ Ejemplo conforme 1 — Anulación explícita en el cliente¶
lista_t *lista = lista_crear();
if (lista == NULL)
{
return -1;
}
lista_destruir(lista);
lista = NULL;Después de la segunda línea, cualquier uso accidental del puntero se detecta
como NULL y no como memoria liberada. La carga de la prueba pasa a la lógica
del programa, no al asignador.
✅ Ejemplo conforme 2 — Destructor que recibe doble puntero¶
/* lista.h */
void lista_destruir(lista_t **lista);
/* lista.c */
void lista_destruir(lista_t **lista)
{
if (lista == NULL || *lista == NULL)
{
return;
}
liberar_nodos((*lista)->cabeza);
free(*lista);
*lista = NULL;
}La interfaz resuelve la asimetría de raíz: recibe la dirección de la variable del cliente y la anula ella misma. El llamador queda libre de recordar la asignación y el patrón es imposible de olvidar.
⚠️ Casos límite¶
lista_destruir(NULL): debe ser un no-op seguro; validá*lista == NULLantes de liberar.Punteros en arreglos: anular el elemento (
recursos[i] = NULL;) tras liberarlo, no el índice.Variables
const: no se pueden reasignar; si el recurso esconst, la anulación ocurre sobre la variable no calificada que lo originó.
Cómo detectarla¶
| Herramienta | Comando | Señal |
|---|---|---|
gaff | gaff check archivo.c | Llamada al destructor sin asignación = NULL posterior. |
gcc / clang | gcc -Wall -Wextra -std=c11 ... | No la detecta por sí solo; -Wuse-after-free ayuda en casos simples. |
valgrind | valgrind ./programa | Invalid read posterior a free, o double free. |
Checklist de autocontrol¶
¿Hay un destructor por cada constructor?
¿Anulé el puntero inmediatamente después del destructor?
¿La interfaz ofrece la variante con
T **cuando corresponde?¿El destructor tolera recibir
NULL?¿Evité volver a usar la variable entre el destructor y la anulación?
Reglas relacionadas¶
0x3002h: Liberá siempre la memoria dinámica y asigná NULL al puntero para mitigar punteros colgantes — regla general de
freemás anulación de punteros.0x3014h: Prohibición de doble liberación de memoria (double free) sobre el mismo puntero — la doble liberación es la consecuencia directa de omitir la anulación.
0x301Dh: Diseñá los Tipos de Datos Abstractos utilizando punteros opacos — la opacidad hace que el puntero sea la única prueba de validez.