Programación en C/Manejo dinámico de memoria

Seguramente durante todo tu recorrido con este libro, especialmente en la sección de punteros, te hablábamos sobre la asignación dinámica de memoria. Como su nombre lo dice, este es una forma de conseguir espacio en memoria, dándote más eficiencia y técnicamente hacer lo que requieras hacer con este.

En esta sección haremos uso de los punteros, por lo que te recomendamos que tengas muy bien establecidos estos conocimientos.

Memoria dinámica

editar

Es memoria que se reserva en tiempo de ejecución. Su principal ventaja frente a la estática, es que su tamaño puede variar durante la ejecución del programa. (En C, el programador es encargado de liberar esta memoria cuando no la utilice más). El uso de memoria dinámica es necesario cuando no se sabe el numero exacto de datos/elementos a tratar.

Memoria estática

editar

Es el espacio en memoria que se crea al declarar variables de cualquier tipo de dato (primitivas [int,char...] o derivados [struct,matrices,punteros...]). La memoria que estas variables ocupan no puede cambiarse durante la ejecución y tampoco puede ser liberada manualmente.

Diferencias, ventajas y desventajas

editar

La memoria reservada de forma dinámica suele estar alojada en el heap o almacenamiento libre, y la memoria estática en el stack o pila (con excepción de los objetos de duración estática, que se verán más adelante, los cuales normalmente se colocan en una zona estática de datos). La pila generalmente es una zona muy limitada. El heap, en cambio, en principio podría estar limitado por la cantidad de memoria disponible durante la ejecución del programa y el máximo de memoria que el sistema operativo permita direccionar a un proceso. La pila puede crecer de forma dinámica, pero esto depende del sistema operativo. En cualquier caso, lo único que se puede asumir es que muy probablemente dispondremos de menor espacio en la pila que en el heap.

Otra ventaja de la memoria dinámica es que se puede ir incrementando durante la ejecución del programa. Esto permite, por ejemplo, trabajar con arreglos dinámicos. Aunque en C, a partir del estándar C99 se permite la creación de arreglos cuyo tamaño se determina en tiempo de ejecución, no todos los compiladores implementan este estándar. Además, se sigue teniendo la limitante de que su tamaño no puede cambiar una vez que se especifica, cosa que sí se puede lograr asignando memoria de forma dinámica.

Una desventaja de la memoria dinámica es que es más difícil de manejar. La memoria estática tiene una duración fija, que se reserva y libera de forma automática. En contraste, la memoria dinámica se reserva de forma explícita y continúa existiendo hasta que sea liberada, generalmente por parte del programador.

La memoria dinámica puede afectar el rendimiento. Puesto que con la memoria estática el tamaño de las variables se conoce en tiempo de compilación, esta información está incluida en el código objeto generado, por lo cual el proceso es muy eficiente. Cuando se reserva memoria de manera dinámica, se tienen que llevar a cabo varias tareas, como buscar un bloque de memoria libre y almacenar la posición y tamaño de la memoria asignada, de manera que pueda ser liberada más adelante. Todo esto representa una carga adicional, aunque esto depende de la implementación y hay técnicas para reducir su impacto.

El lenguaje C y el manejo de la memoria

editar

Todos los objetos tienen un tiempo de vida, es decir, el tiempo durante el cual se garantiza que el objeto exista. En C, existen 3 tipos de duración: estática, automática y asignada. Las variables globales y las variables locales declaradas con el especificador static tienen duración estática. Se crean antes de que el programa inicie su ejecución y se destruyen cuando el programa termina. Las variables locales no static tienen duración automática. Se crean al entrar al bloque en el que fueron declaradas y se destruyen al salir de ese bloque. Duración asignada se refiere a los objetos cuya memoria se reserva de forma dinámica. Como se explicó anteriormente, esta memoria se crea y se debe liberar de forma explícita. Los arreglos de longitud variable de C99 son un caso especial. Tienen duración automática, con la particularidad de que son creados a partir de su declaración.

La biblioteca estándar de C proporciona las funciones malloc, calloc, realloc y free para el manejo de memoria dinámica. Estas funciones están definidas en el archivo de cabecera stdlib.h.

malloc

editar

La función malloc reserva un bloque de memoria y devuelve un puntero void al inicio de la misma. Tiene la siguiente definición:

void *malloc(size_t size);

donde el parámetro size especifica el número de bytes a reservar. En caso de que no se pueda realizar la asignación, devuelve el valor nulo (definido en la macro NULL), lo que permite saber si hubo errores en la asignación de memoria.

Ej:

int *puntero;

char *puntcarc;

puntero=(int *)malloc(4);

puntcarc=(char *)malloc(200);

A continuación se muestra un ejemplo de su uso:

int *i;

/* Reservamos la memoria suficiente para almacenar un int y asignamos su dirección a i */

i = malloc(sizeof(int));

/* Verificamos que la asignación se haya realizado correctamente */
if (i  == NULL) {
	/* Error al intentar reservar memoria */
}

Uno de los usos más comunes de la memoria dinámica es la creación de vectores cuyo número de elementos se define en tiempo de ejecución:

int *vect1, n;
printf("N£mero de elementos del vector: ");
scanf("%d", &n);

/* reservar memoria para almacenar n enteros */
vect1 = malloc(n * sizeof(int));

/* Verificamos que la asignación se haya realizado correctamente */
if (vect1  == NULL) {
	/* Error al intentar reservar memoria */
}

calloc

editar

La función calloc funciona de modo similar a malloc, pero además de reservar memoria, inicializa a 0 la memoria reservada. Se usa comúnmente para arreglos y matrices. Está definida de esta forma:

void *calloc(size_t nmemb, size_t size);

El parámetro nmemb indica el número de elementos a reservar, y size el tamaño de cada elemento. El ejemplo anterior se podría reescribir con calloc de esta forma:

int *vect1, n;
printf("N£mero de elementos del vector: ");
scanf("%d", &n);

/* Reservar memoria para almacenar n enteros */
vect1 = calloc(n, sizeof(int));

/* Verificamos que la asignación se haya realizado correctamente */
if (vect1  == NULL) {
	/* Error al intentar reservar memoria */
}

realloc

editar

La función realloc redimensiona el espacio asignado de forma dinámica anteriormente a un puntero. Tiene la siguiente definición:

void *realloc(void *ptr, size_t size);

Donde ptr es el puntero a redimensionar, y size el nuevo tamaño, en bytes, que tendrá. Si el puntero que se le pasa tiene el valor nulo, esta función actúa como malloc. Si la reasignación no se pudo hacer con éxito, devuelve un puntero nulo, dejando intacto el puntero que se pasa por parámetro. Al usar realloc, se debería usar un puntero temporal. De lo contrario, podríamos tener una fuga de memoria, si es que ocurriera un error en realloc.

Ejemplo de realloc usando puntero temporal:

/* Reservamos 5 bytes */
void *ptr = malloc(5);

/* Redimensionamos el puntero (a 10 bytes) y lo asignamos a un puntero temporal */
void *tmp_ptr = realloc(ptr, 10);

if (tmp_ptr == NULL) {
	/* Error: tomar medidas necesarias */
}

/* Reasignación exitosa. Asignar memoria a ptr */
ptr = tmp_ptr;

Cuando se redimensiona la memoria con realloc, si el nuevo tamaño (parámetro size) es mayor que el anterior, se conservan todos los valores originales, quedando los bytes restantes sin inicializar. Si el nuevo tamaño es menor, se conservan los valores de los primeros size bytes. Los restantes también se dejan intactos, pero no son parte del bloque regresado por la función.

free o cfree

editar

La función free sirve para liberar memoria que se asignó dinámicamente. Si el puntero es nulo, free no hace nada. Además existe la función cfree, que sirve para liberar memoria de los elementos que han sido reservados con calloc(). Tienen la siguiente definición:

void free(void *ptr); void cfree (void *ptr);

El parámetro ptr es el puntero a la memoria que se desea liberar:

int *i;
i = malloc(sizeof(int));

free(i);

Una vez liberada la memoria, si se quiere volver a utilizar el puntero, primero se debe reservar nueva memoria con malloc o calloc:

int *i = malloc(sizeof(int));

free(i);

/* Reutilizamos i, ahora para reservar memoria para dos enteros */
i = malloc(2 * sizeof(int));

/* Volvemos a liberar la memoria cuando ya no la necesitamos */
free(i);

Buenas prácticas

editar

Como se vio en las secciones anteriores, siempre que se reserve memoria de forma dinámica con malloc, realloc o calloc, se debe verificar que no haya habido errores (verificando que el puntero no sea NULL). Cuando se trata de verificar el valor de un puntero (y sólo en ese caso), se puede usar de forma indistinta 0 ó NULL. Usar uno u otro es cuestión de estilo. Como ya se vio, las funciones de asignación dinámica de memoria devuelven un puntero void. Las reglas de C establecen que un puntero void se puede convertir automáticamente a un puntero de cualquier otro tipo, por lo que no es necesario hacer una conversión (cast), como en el siguiente ejemplo:

/* El puntero void devuelto por malloc es convertido explícitamente a puntero int */
int *i = (int *)malloc(sizeof(int));

Aunque no hay un consenso, muchos programadores prefieren omitir la conversión anterior porque la consideran menos segura. Si accidentalmente se olvida incluir el archivo stdlib.h (donde están definidas malloc, calloc, realloc y free) en un programa que use dichas funciones, el comportamiento puede quedar indefinido. Si omitimos la conversión explícita, el compilador lanzará una advertencia. Si, en cambio, realizamos la conversión, el compilador generará el código objeto de forma normal, ocultado el bug.

Una posible razón para usar la conversión explícita es si se escribe código en C que se vaya a compilar junto con código C++, ya que en C++ sí es necesario realizar esa conversión.

En cualquier caso, dado que el manejo de memoria es un tema complejo, y éste es un error muy común, se debe hacer énfasis en que cuando se trabaja con memoria dinámica, siempre se debe verificar que se incluya el archivo stdlib.h.

Tratar de utilizar un puntero cuyo bloque de memoria ha sido liberado con free puede ser sumamente peligroso. El comportamiento del programa queda indefinido: puede terminar de forma inesperada, sobrescribir otros datos y provocar problemas de seguridad. Liberar un puntero que ya ha sido liberado también es fuente de errores.

Para evitar estos problemas, se recomienda que después de liberar un puntero siempre se establezca su valor a NULL.

int *i;
i = malloc(sizeof(int));

free(i);
i = NULL;