← Gestión de Memoria Bloque 2 · La pila y el heap · Tema 7 de 52

Tema 7

El heap y la memoria dinámica

No todo puede decidirse en tiempo de compilación. Cuando el tamaño de los datos se conoce solo al ejecutar el programa, se pide memoria al heap con malloc, calloc o realloc, y se devuelve con free. En este tema verás cómo funcionan estas funciones, cómo se organiza el heap, qué errores son los más comunes y cómo medir su uso en Linux.

1. Qué es el heap

El heap es la región del espacio de direcciones que se usa para memoria dinámica: bloques cuyo tamaño y tiempo de vida se deciden en tiempo de ejecución. A diferencia de la pila, que se gestiona automáticamente por llamadas a funciones, el heap se gestiona manualmente: el programador pide memoria y la devuelve.

El heap crece hacia direcciones crecientes (hacia arriba), justo después de la región de datos. Se administra mediante una estructura de datos (lista libre) que el asignador mantiene; el más común en Linux es el de glibc (ptmalloc2), que estudiaremos en el bloque 10.

Heap es la región de memoria dinámica del proceso, administrada por un asignador (malloc/free) que obtiene memoria del sistema mediante brk/sbrk o mmap.

2. malloc: pedir memoria sin inicializar

malloc(size) reserva un bloque de al menos size bytes y devuelve un puntero a él. El contenido del bloque es indeterminado (basura). Si no hay memoria disponible, devuelve NULL.

uso básico de malloc
#include <stdlib.h>

int *p = malloc(100 * sizeof *p);
if (p == NULL) {
    /* sin memoria: tratar el error */
}
/* usar p[0..99] */
free(p);

Reglas importantes:

3. calloc: pedir e inicializar a cero

calloc(nmemb, size) reserva espacio para nmemb elementos de size bytes y los pone a cero. Es equivalente a malloc(nmemb*size) seguido de memset(..., 0, ...), pero puede ser más eficiente (el sistema puede mapear páginas ya a cero).

calloc
int *p = calloc(100, sizeof *p);   /* 100 enteros a cero */
if (p == NULL) { /* error */ }
/* p[i] == 0 para todo i */

4. realloc: cambiar el tamaño

realloc(ptr, size) cambia el tamaño del bloque apuntado por ptr a size bytes. Puede mover el bloque a otra posición si no hay espacio contiguo. Devuelve el nuevo puntero (que puede ser distinto) o NULL si falla (en cuyo caso el bloque original sigue válido).

realloc con el patrón seguro
int *p = malloc(10 * sizeof *p);
/* ... usar p ... */
int *tmp = realloc(p, 20 * sizeof *p);
if (tmp == NULL) {
    /* fallo: p sigue válido, no lo pierdas */
    free(p);
    return 1;
}
p = tmp;   /* ahora p tiene 20 elementos */

Reglas:

5. free: devolver memoria

free(ptr) devuelve el bloque al asignador para que pueda reutilizarlo. Después de free, el puntero queda dangling (colgante): no debe usarse.

patrón seguro tras free
free(p);
p = NULL;   /* evita uso accidental */

6. Cómo se organiza el heap

El heap no es una lista simple de bloques. El asignador (glibc) mantiene metadatos en cada bloque (cabecera con tamaño y flags) y organiza los bloques libres en estructuras (bins) para reutilizarlos rápidamente. Los bloques asignados al usuario no tienen cabecera visible, pero están precedidos por metadatos.

Organización del heap: bloques con metadatos y datos del usuario Metadatos Datos del usuario Metadatos bloque asignado (malloc)
Cada bloque del heap tiene metadatos (tamaño, flags) junto a los datos del usuario. El asignador usa esos metadatos para gestionar la lista libre.

Por eso nunca se debe escribir antes del puntero devuelto por malloc: se corromperían los metadatos y se podría comprometer el programa.

7. Errores comunes

ErrorQué esConsecuencia
Fuga de memoriaPedir memoria y no liberarla cuando ya no se usa.El heap crece sin límite; el proceso consume más y más RAM.
Uso después de freeAcceder a un bloque ya liberado.Comportamiento indefinido: puede funcionar, corromper datos o crashear.
Doble freeLiberar el mismo bloque dos veces.Corrupción del heap; a menudo explotable para ataques.
Desbordamiento de búferEscribir más allá del final del bloque.Corrupción de metadatos o datos adyacentes; vulnerabilidad clásica.
Leer sin inicializarUsar malloc sin escribir antes de leer.Valores basura; fugas de información.
Puntero colgantePuntero a bloque liberado que se sigue usando.Igual que uso después de free.
El asignador no protege contra estos errores

malloc/free son rápidos porque no verifican cada acceso. Las herramientas de detección (Valgrind, AddressSanitizer) añaden comprobaciones y son indispensables en desarrollo.

8. Experimento: pedir, usar, liberar y medir

El programa siguiente pide bloques de distintos tamaños, los usa, los libera y mide el tiempo. También muestra el efecto de la reutilización:

heap.c
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

static double ahora(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec/1e9; }

int main(void)
{
    const int N = 100000;
    void **ptrs = malloc(N * sizeof *ptrs);
    double t = ahora();
    for (int i = 0; i < N; i++) ptrs[i] = malloc(64);
    printf("malloc %d x 64 B: %.3f s\n", N, ahora() - t);

    t = ahora();
    for (int i = 0; i < N; i++) free(ptrs[i]);
    printf("free  %d bloques: %.3f s\n", N, ahora() - t);

    /* Reutilización: pedir de nuevo debe ser rápido */
    t = ahora();
    for (int i = 0; i < N; i++) ptrs[i] = malloc(64);
    printf("malloc de nuevo: %.3f s (reutilización)\n", ahora() - t);

    for (int i = 0; i < N; i++) free(ptrs[i]);
    free(ptrs);
    return 0;
}
terminal · pedir, liberar y reutilizar
$ gcc -std=c17 -O2 -Wall -Wextra -o heap heap.c
$ ./heap
malloc 100000 x 64 B: 0,018 s
free  100000 bloques: 0,012 s
malloc de nuevo: 0,009 s (reutilización)

La segunda tanda de malloc es más rápida porque el asignador reutiliza los bloques liberados sin pedir memoria nueva al sistema. Esto ilustra por qué free es importante: no solo libera RAM, también acelera futuras asignaciones.

9. El heap y el sistema: brk y sbrk

El asignador no crea memoria de la nada: la pide al núcleo. Las dos llamadas al sistema son brk y sbrk, que mueven el "program break" (el final del heap). Cuando malloc necesita más memoria de la que tiene, llama a sbrk para ampliar el heap; cuando free libera mucho, puede reducirlo.

Para bloques grandes, malloc usa mmap en lugar de brk, creando regiones anónimas independientes que se liberan con munmap. El umbral (M_MMAP_THRESHOLD) es típicamente 128 KiB.

MecanismoCuándo lo usa mallocVentaja
brk/sbrkBloques pequeños y medianosReutilización eficiente; un solo heap contiguo.
mmap anónimoBloques grandes (≥ 128 KiB por defecto)Se libera completamente al hacer free; no fragmenta el heap.

10. Probar con un simulador

Este simulador muestra el heap como una lista de bloques. Puedes pedir bloques de distintos tamaños y liberarlos, observando la fragmentación y la reutilización.

Simulador · asignación en el heap

MedidaValor
Bloques asignados
Memoria total pedida
Bloques libres (reutilizables)

11. Mirar el heap en Linux

terminal · heap y herramientas
$ cat /proc/self/maps | grep heap
55a1b3c06000-55a1b3c27000 rw-p 00000000 00:00 0  [heap]
$ valgrind --leak-check=full ./heap
$ gcc -fsanitize=address -g heap.c -o heap-asan && ./heap-asan
$ cat /proc/PID/smaps | grep -A5 heap

12. Resumen

13. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Qué devuelve malloc si no hay memoria?

NULL. Por eso siempre hay que comprobar el resultado antes de usarlo.

2. ¿Qué diferencia hay entre malloc y calloc?

calloc inicializa la memoria a cero; malloc no inicializa (contenido indeterminado).

3. ¿Por qué no se debe hacer p = realloc(p, n) directamente?

Si realloc falla devuelve NULL y se pierde el puntero original, causando una fuga. Hay que usar una variable temporal.

4. ¿Qué ocurre si se libera un bloque dos veces?

Comportamiento indefinido, normalmente corrupción del heap, que puede causar fallos o vulnerabilidades.

5. ¿Qué es una fuga de memoria?

Pedir memoria y no liberarla cuando ya no se usa, haciendo que el heap crezca sin límite.

6. ¿Cuándo usa malloc mmap en lugar de brk?

Para bloques grandes (por defecto ≥ 128 KiB), creando una región anónima independiente que se libera con munmap.

7. ¿Qué hace free(NULL)?

Nada: es válido y no tiene efecto. Es útil para simplificar el código de limpieza.

8. ¿Qué herramienta detecta fugas de memoria?

Valgrind (con --leak-check=full) y AddressSanitizer (-fsanitize=address).

Para practicar

  1. Modifica heap.c para que libere solo la mitad de los bloques y observa el efecto en el tiempo de la segunda tanda de malloc.
  2. Escribe un programa con una fuga deliberada y ejecútalo con Valgrind. ¿Qué reporta?
  3. Compila un programa con un desbordamiento de búfer con -fsanitize=address. ¿Qué mensaje produce?
  4. Usa realloc para hacer crecer un array dinámico (como un vector). Mide el tiempo para 1.000.000 de inserciones.
  5. Compara malloc(1000000) seguido de memset(0) con calloc(1000000, 1). ¿Cuál es más rápido?
  6. Inspecciona /proc/PID/maps de un programa que pide 200 KiB con malloc. ¿Aparece en el heap o en una región mmap?
  7. Escribe una función que duplique una cadena con malloc y strcpy. Libérala correctamente.