Menu

Fugas de memoria en C: cómo ocurren y cómo encontrarlas

Qué es realmente una fuga, las tres formas en que los programas de C las producen, la disciplina de propiedad que las previene y cómo encontrar las demás con valgrind y -fsanitize=address, terminando con un programa con fugas arreglado paso a paso.

Esta página incluye editores ejecutables: edita, ejecuta y ve el resultado al instante.

Una fuga de memoria no es memoria que se desvaneció. Es memoria que sigue siendo tuya, sigue reservada y que has perdido la capacidad de devolver, porque el último puntero que apuntaba a ella ya no está. Nada revienta. El programa sigue corriendo, un poco más pesado cada vez, hasta que algo acaba fallando en un lugar sin relación.

Esta página cubre cómo surgen las fugas, la disciplina que previene la mayoría y las dos herramientas que encuentran el resto.

Qué aspecto tiene una fuga

Cada iteración sobrescribe block con un puntero nuevo. El bloque anterior sigue reservado; ninguna variable guarda su dirección; nunca podrá liberarse. Tres iteraciones pierden doce kilobytes. Un servidor que haga esto una vez por petición los pierde para siempre, al ritmo al que lleguen las peticiones.

La solución es una línea —free(block); al final del cuerpo— pero reconocer dónde va es la habilidad de verdad.

Cómo ocurren las fugas

1. El puntero perdido

Cualquier asignación a un puntero que todavía guarda la única referencia a un bloque vivo lo pierde.

char *name = malloc(32);
name = malloc(64);     /* los primeros 32 bytes ya son inalcanzables */

El bucle de arriba es el mismo error disfrazado de bucle. También lo es reasignar un campo de una estructura, y también el atajo de realloc de calloc y realloc:

p = realloc(p, n);     /* si falla: p pasa a NULL y el bloque viejo queda huerfano */

2. El retorno temprano

Cada camino de salida de una función tiene que liberar lo que la función ya haya tomado. El que se olvida siempre es un camino de error.

El camino feliz es correcto y el de error tiene la fuga, que es por lo que esto sobrevive a las pruebas: la rama que falla casi nunca se ejecuta durante el desarrollo. La solución es una única sección de limpieza a la que salta cada camino:

Este es el único uso de goto que los programadores de C con experiencia recomiendan activamente. Funciona porque cada puntero empieza en NULL y free(NULL) no hace nada, así que un solo bloque de salida es correcto sin importar hasta dónde llegó la función.

3. Propiedad poco clara

Las fugas más sutiles no son errores de codificación en absoluto: son dos funciones que no se ponen de acuerdo sobre a quién le tocaba.

char *build_message(void);   /* quien llama, libera esto? */
void  store(char *text);     /* store toma la propiedad? */

Si build_message devuelve memoria reservada y store la copia, quien llama debe liberarla. Si store se queda con el puntero, quien llama no debe hacerlo. Nada en el código dice cuál es el caso, así que una de las dos suposiciones se toma dos veces, y obtienes o bien una fuga o bien una doble liberación.

El remedio es una convención, escrita en un comentario junto a cada función que reserva memoria:

/* Devuelve una cadena recien reservada; quien llama debe liberarla. */
char *build_message(void);

/* Toma la propiedad de 'text'; sera liberado por store_free(). */
void store(char *text);

Escribe la regla en la función, no en un documento de diseño. Es el hábito de mayor valor en la gestión de memoria en C.

La disciplina de la propiedad

Cuatro reglas cubren casi todo:

  1. Cada reserva tiene exactamente un dueño: un trozo de código responsable de liberarla.
  2. Empareja cada función que reserva con una que libera. vec_init / vec_free, config_load / config_free. La simetría hace visible una llamada que falta.
  3. Libera en la misma capa que reservó, salvo que el comentario de la función transfiera la propiedad de forma explícita.
  4. Pon el puntero a NULL después de liberarlo, para que un uso accidental posterior reviente en el lugar del fallo en lugar de corromper el montón en silencio.

Encontrar fugas: valgrind

En Linux, valgrind no necesita recompilar nada, aunque los símbolos de depuración hacen el informe legible:

gcc -g -O0 program.c -o program
valgrind --leak-check=full --show-leak-kinds=all ./program

Para el bucle con fuga del inicio de esta página, el informe termina con algo como:

==12345== HEAP SUMMARY:
==12345==     in use at exit: 12,000 bytes in 3 blocks
==12345==   total heap usage: 3 allocs, 0 frees, 12,000 bytes allocated
==12345==
==12345== 12,000 bytes in 3 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2FB0F: malloc (vg_replace_malloc.c:299)
==12345==    by 0x108671: main (program.c:6)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 12,000 bytes in 3 blocks

Léelo de abajo hacia arriba. "definitely lost" significa que al salir no existía ningún puntero al bloque: una fuga real. La traza de llamadas nombra la línea del malloc que lo creó, no la línea donde se perdió, lo que suele bastar para encontrar el free que falta.

Aparecen otras dos categorías:

  • indirectly lost: bloques alcanzables solo a través de un bloque que a su vez se perdió, como los elementos de una lista enlazada con fuga. Arregla el "definitely lost" y estos desaparecen.
  • still reachable: reservados al salir pero con un puntero vivo, típicamente una caché global. No es una fuga en el sentido peligroso, pero vale la pena liberarlos para que el informe quede vacío.

Valgrind también detecta lecturas de memoria sin inicializar y escrituras más allá del final de un bloque, que a menudo es como descubres el error que está detrás de la fuga.

Encontrar fugas: AddressSanitizer

AddressSanitizer viene integrado en GCC y Clang, corre mucho más rápido que valgrind y funciona donde valgrind no (incluido macOS actual):

gcc -g -fsanitize=address -fno-omit-frame-pointer program.c -o program
./program

El informe de fugas se imprime automáticamente al salir:

=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 12000 byte(s) in 3 object(s) allocated from:
    #0 0x7f... in malloc
    #1 0x1086... in main program.c:6

SUMMARY: AddressSanitizer: 12000 byte(s) leaked in 3 allocation(s).

ASan también convierte el uso después de liberar y los desbordamientos de búfer en el montón en abortos inmediatos y claramente etiquetados, en lugar de una corrupción misteriosa más adelante. Compila tus ejecuciones de prueba con él activado; compila las versiones de lanzamiento sin él, ya que cuesta memoria y velocidad.

Si la detección de fugas no se dispara en tu plataforma, define ASAN_OPTIONS=detect_leaks=1 en el entorno antes de ejecutar.

Arreglar un programa con fugas paso a paso

Aquí hay un programa pequeño con tres fugas distintas:

Valgrind reporta tres registros "definitely lost" con tres números de línea distintos. Arreglados uno a uno:

El arreglo 1 es el comentario de propiedad hecho realidad: shout reserva, main libera. El arreglo 2 elimina por completo la reserva duplicada en lugar de liberar la primera: el código más simple es también el correcto. El arreglo 3 agrega el free que faltaba en el retorno temprano; con más reservas en juego, la única etiqueta cleanup: mostrada antes escala mejor que repetir las liberaciones.

Hábitos que previenen fugas

  • Escribe el free inmediatamente después de escribir el malloc, y luego rellena el código de en medio.
  • Dale a cada función que reserva una función que libera a juego.
  • Declara la propiedad en un comentario en cualquier función que devuelva o reciba un puntero que ella reservó.
  • Usa un único bloque de salida cleanup: en las funciones que manejan varias reservas.
  • Ejecuta tus pruebas bajo -fsanitize=address por rutina, no solo cuando algo pinta mal.
  • Trata "definitely lost: 0 bytes" como parte de una ejecución de pruebas exitosa.

Preguntas frecuentes

¿Qué es una fuga de memoria en C?

Memoria que reservaste con malloc y que ya no puedes liberar, porque nada en el programa sigue apuntando a ella. El bloque queda reservado durante toda la vida del proceso. No es un fallo: el programa sigue funcionando, solo que usando más memoria en cada pasada hasta que al final se le acaba.

¿Cómo encuentro fugas de memoria en C?

Ejecuta el programa bajo valgrind: valgrind --leak-check=full ./program. Reporta cada bloque que seguía reservado al salir, con la traza del malloc que lo creó. En macOS o donde valgrind no esté disponible, compila con -fsanitize=address y el mismo informe sale al terminar.

¿Qué causa las fugas de memoria en C?

Tres patrones cubren casi todas: sobrescribir el único puntero a un bloque (incluido p = realloc(p, n) cuando falla), retornar antes de tiempo desde una función que ya reservó memoria, y una propiedad poco clara: dos funciones que dan por hecho que la otra lo libera, así que ninguna lo hace.

¿Importan las fugas de memoria si el programa termina de todas formas?

Para un programa que se ejecuta una vez y termina, el sistema operativo recupera todo, así que el impacto práctico es nulo. Importa en cualquier cosa de larga duración —un servidor, un bucle de juego, un demonio— donde una fuga por petición crece sin límite. Libera de forma consistente igualmente: un informe con fugas es ruido que esconde las que sí importan.

Coddy programming languages illustration

Aprende a programar con Coddy

COMENZAR