Una de las anomalías más comunes cuando se gestiona la
memoria de forma explícita es lo que se conoce como “fuga de
memoria”. Esta situación ocurre cuando un programa obtiene memoria
dinámica, y el valor del puntero que devuelve el sistema, por error, se
pierde. En tal caso, ya no es posible invocar a la función free
con ese puntero, y la porción de memoria se queda reservada por lo que resta
de ejecución. Como ejemplo de fuga de memoria analicemos el siguiente
fragmento de código.
char *string; string = (char *)malloc(100); string = NULL;
La primera línea declara un puntero a carácter. En la
segunda se reserva un espacio de 100 bytes. El gestor de memoria devuelve un
puntero al comienzo de ese bloque y se almacena en la variable
string
. En ese momento, la dirección de ese bloque no está
almacenada en ningún otro sitio. La línea siguiente asigna el valor
NULL
al mismo puntero. ¿Qué ha sucedido con la dirección de
memoria de la porción que se acaba de reservar? Se ha perdido y no hay forma
alguna de recuperarla, porque string
era la única copia de
ese valor. Como consecuencia, la porción de memoria reservada seguirá
marcada como ocupada por el resto de ejecución del programa. La memoria se
ha fugado.
La principal consecuencia de una fuga de memoria, por tanto, es que esa porción no se puede utilizar, se ha perdido. Esto es equivalente a que la memoria disponible para la ejecución del programa se haya reducido. Los efectos de una fuga de memoria dependen del lugar en el código en el que se produzca. Si en un programa se fuga una única porción de unos cuantos bytes, es posible que su efecto pase desapercibido. Sin embargo, si la pérdida de memoria se produce en un lugar que se ejecuta un número muy elevado de veces, el efecto puede ser mucho más notorio. Fíjate en el siguiente fragmento de programa:
#define MILLION 1000000 char *table[MILLION]; for (i = 0; i < MILLION; i++) { table[i] = (char *)malloc(100); table[i] = NULL; }
La fuga de memoria se produce en un lugar que forma parte de un bucle que se ejecuta un millón de veces. En este bucle se fugan casi 100 Megabytes de memoria.
Las fugas de memoria no se producen en situaciones tan obvias como las descritas anteriormente, sino que aparecen en lugares del código inesperados debidos a despistes en la manipulación de punteros. El problema de las fugas de memoria en C es tan complicado de solventar que han aparecido herramientas especializadas, tanto comerciales como de código libre, especialmente concebidas para analizar un programa y detectar fugas.
Una situación típica de fuga de memoria es cuando se
manipulan estructuras de datos encadenadas. En una estructura se almacenan
punteros obtenidos mediante llamadas a malloc
y en ellos a su
vez se almacenan más punteros obtenidos de esta manera. La liberación de la
memoria que ocupan estas estructuras de datos ha de programarse con sumo
cuidado. El siguiente fragmento de código ilustra este problema.
struct contact_information { char *name, *lastname; int age; }; struct contact_information *agenda; int i; agenda = (struct contact_information *)calloc(100, sizeof(struct contact_information)); for (i = 0; i < 100; i++) { agenda[i].name = (char *)malloc(10); agenda[i].lastname = (char *)malloc(30); agenda.age = 0; } free(agenda);
La variable agenda
se reserva con espacio
suficiente para almacenar 100 estructuras del tipo
struct contact_information
. En el bucle, los dos primeros
campos de cada una de las estructuras se inicializa con dos punteros que se
obtienen mediante malloc
. Al terminar el bucle, la llamada
free(agenda)
libera el espacio reservado para la tabla, pero no
el que se ha reservado para las cadenas de texto de cada uno de sus
elementos. La forma correcta de liberar la estructura es igualmente con un
bucle que atraviese la tabla y libere cada campo por separado con una
llamada a free
.
Las dos reglas a respetar en cualquier programa en C en lo referente a la gestión dinámica de memoria son:
Toda porción reservada de forma dinámica (con
malloc
, calloc
o realloc
) debe
ser liberada mediante una llamada a free
.
Si un programa llega a su última instrucción y tiene bloques de memoria dinámica sin liberar, se considera que el programa es erróneo.
Desafortunadamente, no hay una técnica concreta para evitar las fugas de memoria, pero sí hay herramientas que dado un programa lo analizan para ofrecerte un informe sobre qué memoria se ha fugado (si ha habido alguna). Para darte una idea de la dificultad de este problema, cuando las primeras herramientas de detección de fugas aparecieron, se utilizaron para analizar aplicaciones que se consideraban sólidas y maduras, y para sorpresa de sus diseñadores, se detectaron fugas que hasta el momento ningún programador había detectado.