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 0x3001h: Siempre verificá la asignación exitosa de memoria dinámica

Memoria, punteros y tipos (0x30XX)

Universidad Nacional de Río Negro

0x3001h: Siempre verificá la asignación exitosa de memoria dinámica

Enunciado normativo

Toda llamada a malloc, calloc o realloc DEBE ser seguida inmediatamente por una comprobación del resultado contra NULL, antes de desreferenciar el puntero o de usarlo en cualquier cálculo.

¿Por qué existe esta regla?

El problema

El asignador puede fallar: el heap se agota, el sistema niega una página o el tamaño es irrazonable. Entonces malloc, calloc y realloc devuelven NULL, una dirección que no apunta a ningún objeto válido. Desreferenciarla es comportamiento indefinido y suele terminar en SIGSEGV; la verificación convierte ese fallo del entorno en un error controlado.

Consecuencias de violarla

Tipo de consecuenciaEfecto concreto
Comportamiento indefinidoDesreferenciar NULL es UB (C11 §6.5.3.2); suele terminar en SIGSEGV.
Bug silenciosoUna cuenta que asume “siempre hay memoria” produce datos corruptos meses después.
RobustezEl programa no distingue entre “sin resultados” y “sin memoria”.

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

C11 §7.22.3 especifica que las funciones de asignación devuelven un puntero nulo si no pueden satisfacer la solicitud. La cátedra trata esa condición como parte del contrato: quien falla debe poder propagarlo, y quien llama, decidir. La regla no exige abortar, exige decidir explícitamente.

Alcance y excepciones

Aplica a toda asignación dinámica, incluidas las de un constructor de TAD o una reasignación intermedia. No aplica a memoria estática ni a variables automáticas. El caso malloc(0) se discute en casos límite.

Ejemplos exhaustivos

❌ Contraejemplo 1 — Desreferencia inmediata sin comprobar

nodo_t *nuevo = malloc(sizeof(*nuevo));
nuevo->dato = valor;
nuevo->sig = NULL;

Por qué falla: si el heap no tiene espacio, nuevo es NULL y la asignación a nuevo->dato escribe en la dirección cero. El chequeo debe ir inmediatamente después de malloc, no al final de la función.

❌ Contraejemplo 2 — Comprobación después de usar el recurso

arreglo = malloc(cantidad * sizeof(*arreglo));
inicializar(arreglo, cantidad);
if (arreglo == NULL)
{
    return ERROR_MEMORIA;
}

Por qué falla: la verificación llega tarde. inicializar recibe un puntero nulo y lo desreferencia antes de que el if se evalúe. La comprobación debe ser inmediata.

✅ Ejemplo conforme 1 — Verificación inmediata y propagación

nodo_t *nodo_crear(int valor)
{
    nodo_t *nuevo = malloc(sizeof(*nuevo));
    if (nuevo == NULL)
    {
        return NULL;
    }
    nuevo->dato = valor;
    nuevo->sig = NULL;
    return nuevo;
}

La función documenta su contrato: “devuelve NULL si no hay memoria”. El llamador decide si aborta, reintenta o degrada la funcionalidad, conforme a 0x3009h: Documentá explícitamente los casos en que una función puede retornar NULL.

✅ Ejemplo conforme 2 — Envoltorio centralizado de asignación

static void *reservar(size_t cantidad)
{
    void *bloque = malloc(cantidad);
    if (bloque == NULL)
    {
        fprintf(stderr, "Sin memoria para %zu bytes\n", cantidad);
        exit(EXIT_FAILURE);
    }
    return bloque;
}

Cuando todo el módulo tiene una política única de fallo (abortar), conviene centralizarla. Así el chequeo aparece una sola vez y ningún camino lo olvida. La verificación sigue existiendo aunque el llamador ya no la repita.

⚠️ Casos límite

Cómo detectarla

HerramientaComandoSeñal
gaffgaff check archivo.cmalloc/calloc/realloc sin if (... == NULL) asociado.
gcc / clanggcc -Wall -Wextra -std=c11 -fanalyzer ...dereference of possibly-NULL en el flujo de asignación.
valgrindvalgrind ./programaInvalid write en dirección 0x0 cuando el fallo se fuerza.

Checklist de autocontrol

Reglas relacionadas

Antipatrón: Uso de memoria dinámica sin validar retorno a NULL

Síntoma en el código del estudiante

El patrón se reconoce porque la llamada a malloc, calloc o realloc y el primer uso del puntero aparecen pegados, sin ninguna pregunta intermedia sobre el valor devuelto:

int *p = malloc(10 * sizeof(*p));
p[0] = 42;

No hay if (p == NULL), ni if (!p), ni una cláusula de guarda. El estudiante asume que “siempre hay memoria” porque en su máquina de escritorio la reserva casi nunca falla.

Diagnóstico

Mecanismo del defecto

malloc no garantiza el éxito. Cuando el asignador no encuentra un bloque libre del tamaño pedido, devuelve el puntero nulo (NULL). Ese valor no es una dirección válida de objeto: es la constante (void *)0.

El operador de indirección *p (o el subíndice p[0], que es azúcar de *(p + 0)) exige que p apunte a un objeto. Si p vale NULL, la operación no tiene un objeto que leer ni escribir: el estándar la declara directamente comportamiento indefinido (C11 §6.5.3.2p4). En un sistema con memoria virtual, la dirección 0 suele no estar mapeada, por lo que el hardware dispara una falla de página irrecuperable y el proceso muere con SIGSEGV; no obstante, el estándar no promete ese desenlace.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
int *crear_vector(size_t n)
{
    int *p = malloc(n * sizeof(*p));
    for (size_t i = 0; i < n; i++) {
        p[i] = 0;
    }
    return p;
}
✅ Código refactorizado
int *crear_vector(size_t n)
{
    int *p = malloc(n * sizeof(*p));
    if (p == NULL) {
        return NULL;
    }

    for (size_t i = 0; i < n; i++) {
        p[i] = 0;
    }
    return p;
}

La guarda se ubica inmediatamente después de la reserva y antes de todo acceso; el llamador recibe NULL y decide qué hacer (regla 0x3009h).

⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ ./programa
Segmentation fault (core dumped)

# Con AddressSanitizer:
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
The signal is caused by a READ memory access.
#0 0x... in crear_vector programa.c:5

Checklist de verificación

Reglas relacionadas

Antipatrón: Desreferencia inmediata tras realloc

Síntoma en el código del estudiante

El resultado de realloc se usa enseguida, sin preguntar si la reasignación tuvo éxito:

ptr = realloc(ptr, nuevo_tam);
ptr[0] = 42;

El acceso sigue en la línea inmediatamente posterior, sin ningún if.

Diagnóstico

Mecanismo del defecto

realloc comparte con malloc el contrato de retorno: devuelve NULL cuando no puede satisfacer el nuevo tamaño. En esa línea, ptr recibe el puntero nulo y la desreferencia siguiente (ptr[0] = 42) actúa sobre NULL: comportamiento indefinido y, en la práctica, caída por SIGSEGV.

El defecto se agrava porque combina dos problemas: además de desreferenciar el nulo, se perdió la referencia al bloque original (el realloc devolvió NULL y el bloque viejo sigue vivo pero inaccesible), es decir, una fuga. Ese aspecto de propiedad lo cubre 0x3015h.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
int *agregar(int *ptr, size_t n)
{
    ptr = realloc(ptr, n * sizeof(*ptr));
    ptr[0] = 42;
    return ptr;
}
✅ Código refactorizado
int *agregar(int *ptr, size_t n)
{
    int *tmp = realloc(ptr, n * sizeof(*tmp));
    if (tmp == NULL) {
        return ptr;   /* conserva el bloque original */
    }
    ptr = tmp;
    ptr[0] = 42;
    return ptr;
}
✅ Guarda explícita del caso nulo
int *tmp = realloc(ptr, n * sizeof(*tmp));
if (!tmp) {
    return NULL;
}
ptr = tmp;
ptr[0] = 42;
⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ ./programa
Segmentation fault (core dumped)

# Con AddressSanitizer:
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
#0 0x... in agregar programa.c:3

Checklist de verificación

Reglas relacionadas

Antipatrón: Asignación de retorno de malloc() a variable no puntero

Síntoma en el código del estudiante

El resultado de malloc se guarda en una variable entera:

int addr = malloc(sizeof(int));

El identificador no lleva asterisco y su tipo es int, mientras que malloc retorna una dirección.

Diagnóstico

Mecanismo del defecto

malloc devuelve void *. Asignarlo a un int sin cast viola las restricciones de la asignación (§6.5.16.1): un puntero no es una expresión aritmética. El compilador está obligado a emitir un diagnóstico; GCC y Clang lo hacen con -Wint-conversion, que suele venir activado por -Wall.

Cuando el estudiante “arregla” el diagnóstico agregando un cast (int addr = (int)malloc(...)), el problema se vuelve peor pero silencioso: en una arquitectura de 64 bits la dirección tiene 64 bits y el int, 32. La conversión trunca los 32 bits altos. addr guarda una dirección corrupta que, al volver a convertirse en puntero, apunta a cualquier lado.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
int addr = malloc(sizeof(int));
*(int *)addr = 42;   /* dirección truncada y cast forzado */
✅ Código refactorizado
int *ptr = malloc(sizeof(*ptr));
if (ptr == NULL) {
    return NULL;
}
*ptr = 42;
✅ Si de verdad se necesita una dirección numérica
#include <stdint.h>

int *ptr = malloc(sizeof(*ptr));
if (ptr == NULL) {
    return NULL;
}
uintptr_t direccion = (uintptr_t)ptr;   /* tipo capaz de contener un puntero */

uintptr_t es el único entero garantizado para almacenar una dirección; aun así, en la cátedra solo se admite con una justificación explícita.

⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ gcc -Wall -Wextra -std=c11 programa.c
programa.c:3:15: warning: initialization of 'int' from 'void *' makes integer
 from pointer without a cast [-Wint-conversion]
    3 |     int addr = malloc(sizeof(int));

$ ./programa
Segmentation fault (core dumped)

Checklist de verificación

Reglas relacionadas

Antipatrón: Comprobación de puntero nulo posterior a su desreferencia

Síntoma en el código del estudiante

El acceso ocurre primero y la pregunta por NULL después:

*ptr = 10;
if (ptr != NULL) {
    /* ... */
}

La verificación llegó tarde: está en la línea 2, pero el uso está en la 1.

Diagnóstico

Mecanismo del defecto

La comprobación de NULL solo tiene valor si protege al primer acceso. Si ptr era nulo, la desreferencia de la línea anterior ya disparó comportamiento indefinido: en la práctica, SIGSEGV. Cuando el flujo llega al if, o el programa ya murió o el acceso “funcionó” por casualidad, y en ninguno de los dos casos el if aporta protección.

Es habitual que este orden invierta aparezca combinado con una asignación sin verificación (0x3001h): el estudiante escribe el uso, luego “por las dudas” agrega el if, sin advertir que el daño ya está hecho. También puede aparecer con el patrón if (ptr != NULL) { ... } else { ... } donde el uso indebido está fuera del if.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
*ptr = 10;
if (ptr != NULL) {
    printf("%d\n", *ptr);
}
✅ Código refactorizado — guarda primero
if (ptr == NULL) {
    return -1;
}
*ptr = 10;
printf("%d\n", *ptr);
✅ Estructura con cláusula de guarda
int inicializar(int *ptr)
{
    if (ptr == NULL) {
        return -1;
    }

    *ptr = 10;
    return 0;
}

La guarda al inicio del bloque es el patrón que la regla 0x2001h prefiere: elimina la anidación y deja claro dónde empieza el camino feliz.

⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ ./programa
Segmentation fault (core dumped)

# Con AddressSanitizer:
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
#0 0x... in main programa.c:1
# la línea señalada es el uso, no el if posterior

Checklist de verificación

Reglas relacionadas

Antipatrón: Asignación múltiple a malloc en bucle sin liberación ante fallos parciales

Síntoma en el código del estudiante

En una reserva por filas, el chequeo de NULL dentro del bucle no libera lo ya asignado:

for (int i = 0; i < n; i++) {
    mat[i] = malloc(m * sizeof(int));
    if (mat[i] == NULL) {
        return NULL;
    }
}

Si falla la fila k, las filas 0..k-1 quedan sin dueño.

Diagnóstico

Mecanismo del defecto

Una matriz dinámica se construye con una reserva por fila. El bucle avanza asignando bloques independientes. Si una reserva intermedia falla, el retorno inmediato abandona el bucle, pero las reservas anteriores siguen vivas en el heap y ya nadie conserva sus punteros: son fugas irrecuperables.

La cantidad de memoria perdida crece con la cantidad de filas asignadas antes del fallo. En un programa que reintenta la operación, cada intento fallido filtra otro bloque completo. Peor: si el fallo se debió a falta de memoria, la fuga agrava la escasez.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
int **crear_matriz(size_t n, size_t m)
{
    int **mat = malloc(n * sizeof(*mat));
    if (mat == NULL) {
        return NULL;
    }
    for (size_t i = 0; i < n; i++) {
        mat[i] = malloc(m * sizeof(*mat[i]));
        if (mat[i] == NULL) {
            return NULL;   /* fuga de mat[0..i-1] y de mat */
        }
    }
    return mat;
}
✅ Código refactorizado — limpieza en orden inverso
int **crear_matriz(size_t n, size_t m)
{
    int **mat = malloc(n * sizeof(*mat));
    if (mat == NULL) {
        return NULL;
    }

    for (size_t i = 0; i < n; i++) {
        mat[i] = malloc(m * sizeof(*mat[i]));
        if (mat[i] == NULL) {
            for (size_t j = 0; j < i; j++) {
                free(mat[j]);
            }
            free(mat);
            return NULL;
        }
    }
    return mat;
}

La liberación recorre las filas ya asignadas (j < i) y luego libera el arreglo de punteros; ese orden inverso es el que exige la regla 0x300Fh.

✅ Alternativa — un único bloque contiguo
int *datos = malloc(n * m * sizeof(*datos));

Con un solo malloc no hay fallo parcial posible; simplifica la propiedad y es preferible cuando no hace falta que las filas sean intercambiables.

⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ valgrind --leak-check=full ./programa
==1234==    definitely lost: 320 bytes in 40 blocks
==1234==      by 0x...: crear_matriz (programa.c:10)

Checklist de verificación

Reglas relacionadas

Antipatrón: Desreferencia condicional de puntero local sin inicializar

Síntoma en el código del estudiante

Un puntero local se declara sin inicializar y se usa en una rama condicional:

int *p;
if (condicion) {
    *p = 10;
}

La variable no tiene valor conocido antes del uso.

Diagnóstico

Mecanismo del defecto

Una variable automática sin inicializar contiene basura: los bytes que quedaron en esa posición de la pila del uso anterior. Para un puntero, ese valor residual se interpreta como una dirección. Es un puntero salvaje (wild pointer): no es NULL ni apunta a un objeto válido.

Desreferenciarlo escribe en una dirección arbitraria. Puede caer si la página no está mapeada (SIGSEGV) o, peor, escribir dentro de una página válida y corromper datos del propio programa. La rama if (condicion) no protege nada: solo decide si el error ocurre.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
int *p;
if (condicion) {
    *p = 10;
}
✅ Código refactorizado — reservar y verificar
int *p = NULL;
if (condicion) {
    p = malloc(sizeof(*p));
    if (p == NULL) {
        return -1;
    }
    *p = 10;
}
✅ Puntero a variable existente
int valor = 0;
int *p = NULL;
if (condicion) {
    p = &valor;   /* apunta a un objeto con vida conocida */
    *p = 10;
}
⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ gcc -Wall -Wextra -std=c11 programa.c
programa.c:2:9: warning: 'p' is used uninitialized [-Wuninitialized]

$ ./programa
Segmentation fault (core dumped)

Checklist de verificación

Reglas relacionadas

Antipatrón: Desreferencia directa tras retorno de realloc sin asignación temporal

Síntoma en el código del estudiante

El retorno de realloc se desreferencia en la misma expresión, sin variable intermedia:

*((int *)realloc(p, n)) = 42;

No hay forma de preguntar si la reasignación falló porque el puntero ni siquiera se guardó.

Diagnóstico

Mecanismo del defecto

La expresión anida tres operaciones: realloc, el cast a int * y la desreferencia con *. Si realloc devuelve NULL (no pudo reasignar), el cast convierte el puntero nulo y el * lo desreferencia: comportamiento indefinido y caída inmediata.

Además, el valor de retorno se descarta después de la sentencia. La variable p no se actualiza: si la reasignación movió el bloque a otra dirección, p sigue apuntando al bloque viejo, que realloc ya liberó. El resultado es un puntero colgante hacia memoria liberada, un use-after-free latente.

O sea, la misma línea concentra los dos defectos: desreferencia sin verificar y pérdida de la referencia actualizada.

Consecuencia observable

Fundamento en el estándar C11

Corrección idiomática

❌ Código con el antipatrón
*((int *)realloc(p, n * sizeof(int))) = 42;
✅ Código refactorizado
int *tmp = realloc(p, n * sizeof(*tmp));
if (tmp == NULL) {
    /* p conserva el bloque original */
    return -1;
}
p = tmp;
*p = 42;
✅ Variante con índice
int *tmp = realloc(p, n * sizeof(*tmp));
if (tmp == NULL) {
    return -1;
}
p = tmp;
p[0] = 42;

Guardar, verificar, actualizar y recién entonces usar: ese es el orden que materializa las reglas 0x3015h y 0x3001h juntas.

⚠️ Casos límite

Errores típicos al compilar o ejecutar

$ ./programa
Segmentation fault (core dumped)

# Con AddressSanitizer:
ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000
#0 0x... in main programa.c:1

# Si el bloque se movió y no se actualizó p:
ERROR: AddressSanitizer: heap-use-after-free on address 0x...

Checklist de verificación

Reglas relacionadas