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.
#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:
- Comprueba siempre que
mallocno devuelvaNULL. - El bloque puede ser mayor que el pedido (alineación, metadatos). No asumas el tamaño exacto.
- La memoria no se inicializa: leerla antes de escribirla es comportamiento indefinido.
- El puntero devuelto está alineado para cualquier tipo (típicamente a 16 bytes).
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).
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).
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:
- Nunca hagas
p = realloc(p, n)directamente: si falla, pierdes el puntero original y hay fuga. - Si
ptresNULL,realloc(NULL, size)equivale amalloc(size). - Si
sizees 0, el comportamiento es indefinido en C (glibc lo trata como free). - El contenido original se preserva hasta el mínimo de los dos tamaños.
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.
free(p);
p = NULL; /* evita uso accidental */
free(NULL)es válido y no hace nada.- Liberar un bloque ya liberado (double free) es comportamiento indefinido y puede corromper el heap.
- Liberar un puntero que no vino de malloc/calloc/realloc es comportamiento indefinido.
- La memoria liberada no se "borra": el contenido sigue ahí hasta que se reutilice.
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.
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
| Error | Qué es | Consecuencia |
|---|---|---|
| Fuga de memoria | Pedir 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 free | Acceder a un bloque ya liberado. | Comportamiento indefinido: puede funcionar, corromper datos o crashear. |
| Doble free | Liberar el mismo bloque dos veces. | Corrupción del heap; a menudo explotable para ataques. |
| Desbordamiento de búfer | Escribir más allá del final del bloque. | Corrupción de metadatos o datos adyacentes; vulnerabilidad clásica. |
| Leer sin inicializar | Usar malloc sin escribir antes de leer. | Valores basura; fugas de información. |
| Puntero colgante | Puntero a bloque liberado que se sigue usando. | Igual que uso después de free. |
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:
#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;
}
$ 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.
| Mecanismo | Cuándo lo usa malloc | Ventaja |
|---|---|---|
| brk/sbrk | Bloques pequeños y medianos | Reutilización eficiente; un solo heap contiguo. |
| mmap anónimo | Bloques 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
| Medida | Valor |
|---|---|
| Bloques asignados | |
| Memoria total pedida | |
| Bloques libres (reutilizables) |
11. Mirar el heap en Linux
$ 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
/proc/PID/mapsmuestra la región[heap].valgrind --leak-check=fulldetecta fugas y usos inválidos.-fsanitize=address(AddressSanitizer) detecta errores en tiempo de ejecución con bajo overhead./proc/PID/smapsda el RSS y otros detalles del heap.
12. Resumen
- El heap es la región de memoria dinámica, gestionada por malloc/free.
mallocreserva sin inicializar;callocreserva e inicializa a cero;realloccambia el tamaño;freedevuelve.- Hay que comprobar siempre el resultado de malloc/realloc y no usar el puntero tras free.
- Los errores comunes son fugas, uso después de free, doble free y desbordamientos.
- El asignador obtiene memoria del sistema con brk/sbrk (bloques pequeños) o mmap (bloques grandes).
- Valgrind y AddressSanitizer son indispensables para detectar errores de memoria.
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
- Modifica
heap.cpara que libere solo la mitad de los bloques y observa el efecto en el tiempo de la segunda tanda de malloc. - Escribe un programa con una fuga deliberada y ejecútalo con Valgrind. ¿Qué reporta?
- Compila un programa con un desbordamiento de búfer con
-fsanitize=address. ¿Qué mensaje produce? - Usa
reallocpara hacer crecer un array dinámico (como un vector). Mide el tiempo para 1.000.000 de inserciones. - Compara
malloc(1000000)seguido dememset(0)concalloc(1000000, 1). ¿Cuál es más rápido? - Inspecciona
/proc/PID/mapsde un programa que pide 200 KiB con malloc. ¿Aparece en el heap o en una región mmap? - Escribe una función que duplique una cadena con malloc y strcpy. Libérala correctamente.