memoria dinamica - lenguaje c

10
Programaci´ on Modular. ETSIT. 1 o C. Apuntes del profesor Juan Falgueras Cano Curso 2005 2 Memoria din´ amica Contenido 2. Memoria din´ amica 1 2.1. Gesti´ on din´ amica de la memoria ............................ 1 2.1.1. Punteros ...................................... 1 2.2. Punteros en pseudoc´ odigo ................................ 4 2.2.1. Utilidad de los punteros ............................. 4 2.2.2. Solicitud din´ amica de memoria ......................... 4 2.2.3. C++ ........................................ 5 2.2.4. Arrays din´ amicos ................................. 6 2.3. Estructuras de datos recursivas ............................. 6 2.4. Listas lineales de de nodos con simple y doble enlace, cabeceras ........... 7 2.5. Listas posicionales y ordenadas ............................. 9 2. Memoria din´ amica Hasta ahora todos los tipos de datos predefinidos (enteros, reales, caracteres, booleanos) y definidos por el usuario (enumerados, y estructuras de datos arrays y registros) han sido de tipo est´ atico. ¿Qu´ e significa esto? Que su ubicaci´ on y capacidad de almacenamiento quedan ıgidamente definidos desde el momento en que se declaran y no puede cambiar durante el funcionamiento del programa. Esto tiene la ventaja obvia de ser muy f´ aciles de localizar en la memoria interna y tambi´ en de simplificar y aligerar el c´ odigo de los programas ejecutables. Con los punteros las cosas no cambian: son est´ aticos. Una variable de tipo puntero es una variable est´ atica, pero. . . la memoria a la que apuntan puede que no. Vamos a ver en este tema c´ omo se puede acceder directamente a la memoria interna libre del sistema y c´ omo se pueden conseguir y devolver montones de memoria seg´ un se necesiten a lo largo de la ejecucion de partes del programa. 2.1. Gesti´ on din´ amica de la memoria Las variables en los programas se declaran mediante un tipo (modo de codificaci´ on) y un nombre de variable. As´ ı para nosotros una variable es un identicador que se refiere a un espacio reservado de una manera definitiva (est´ aticamente) por el compilador en alg´ un lugar (que hasta ahora no nos ha interesado). 2.1.1. Punteros Un puntero es una variable est´ atica (como las vistas hasta ahora) pero destinada a guardar direcciones de memoria de otras varibles. Lo que esto significa es que las direcciones de memoria pueden ser guardadas y manipuladas en nuestros programas. (Ver Figura 2) Una variable puntero (que no lo olvidemos, son en realidad est´ aticas siempre) puede guardar una direcci´ on de memoria. Las cuestiones ahora son: ¿QU ´ E di- recci´ on de memoria? , ¿C ´ OMO obtener direcciones de la memoria interna?, ¿PARA QU ´ E guardar esas direcciones?

Upload: javier-tomasini

Post on 25-Jul-2015

87 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Memoria Dinamica - Lenguaje C

Programacion Modular. ETSIT. 1o C.Apuntes del profesor Juan Falgueras Cano

Curso 2005

2Memoria dinamica

Contenido

2. Memoria dinamica 12.1. Gestion dinamica de la memoria . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1

2.1.1. Punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.2. Punteros en pseudocodigo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4

2.2.1. Utilidad de los punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42.2.2. Solicitud dinamica de memoria . . . . . . . . . . . . . . . . . . . . . . . . . 42.2.3. C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52.2.4. Arrays dinamicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

2.3. Estructuras de datos recursivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62.4. Listas lineales de de nodos con simple y doble enlace, cabeceras . . . . . . . . . . . 72.5. Listas posicionales y ordenadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

2. Memoria dinamica

Hasta ahora todos los tipos de datos predefinidos (enteros, reales, caracteres, booleanos) ydefinidos por el usuario (enumerados, y estructuras de datos arrays y registros) han sido detipo estatico. ¿Que significa esto? Que su ubicacion y capacidad de almacenamiento quedanrıgidamente definidos desde el momento en que se declaran y no puede cambiar durante elfuncionamiento del programa. Esto tiene la ventaja obvia de ser muy faciles de localizar en lamemoria interna y tambien de simplificar y aligerar el codigo de los programas ejecutables.

Con los punteros las cosas no cambian: son estaticos. Una variable de tipo puntero es unavariable estatica, pero. . . la memoria a la que apuntan puede que no.

Vamos a ver en este tema como se puede acceder directamente a la memoria interna libre delsistema y como se pueden conseguir y devolver montones de memoria segun se necesiten a lolargo de la ejecucion de partes del programa.

2.1. Gestion dinamica de la memoria

Las variables en los programas se declaran mediante un tipo (modo de codificacion) y unnombre de variable. Ası para nosotros una variable es un identicador que se refiere a un espacioreservado de una manera definitiva (estaticamente) por el compilador en algun lugar (que hastaahora no nos ha interesado).

2.1.1. Punteros

Un puntero es una variable estatica (como las vistas hasta ahora) pero destinada aguardar direcciones de memoria de otras varibles.

Lo que esto significa es que las direcciones de memoria pueden ser guardadas y manipuladas ennuestros programas. (Ver Figura 2) Una variable puntero (que no lo olvidemos, son en realidadestaticas siempre) puede guardar una direccion de memoria. Las cuestiones ahora son: ¿QUE di-reccion de memoria? , ¿COMO obtener direcciones de la memoria interna?, ¿PARA QUE guardaresas direcciones?

Page 2: Memoria Dinamica - Lenguaje C

2.1 Gestion dinamica de la memoria 2

33a=33;

RAM

?

023

1

Figura 1: Hasta ahora no nos hemos preocupado de saber donde el compilador ubicabalas variables estaticas que hemos usado.

3FA04p

*p 33

p

*p 33

3FA00

3FA04

3FA08

00000

3FA04 001A2

Figura 2: Aunque un puntero es una variable monolıtica que apunta a una zona dememoria con otras variables; el mismo, el puntero es tambien una variablemas en memoria.

Para responder a estas preguntas veremos primero un metodo basico para obtener direccionesde memoria; despues veremos un sistema de localizar bloques de memoria y finalmente, con estohabran quedado respondidas las tres preguntas.

No todos los lenguajes de programacion se arriesgan a permitir la gestion directa de la memoriamediante punteros1. En C/C++:

typedef int *TPuntEntero; // o int* TPuntEntero; es lo mismo/* variables */ int *punt;TPuntEntero a, b;

Una cuestion muy importante respecto a los punteros, y que algunos la toman como diferenciaentre mera direccion y puntero de verdad, es el hecho de que un puntero

siempre lleva asociado un tipo al que apuntar.

Esto es, un puntero no es solo una direccion de memoria interna, para el compilador, tambienasociamos con esas direcciones un tipo de datos. Esto va a ser fundamental para ayudar al compi-lador a disciplinar nuestro programa, en el sentido de que si declaramos un puntero a entero, nopodemos despues asignarle la direccion de un numero real, por ejemplo.

En principio los valores de las direcciones, como las direcciones de cualquier guıa de telefo-nos, por ejemplo, son anonimas y meras referencias geograficas, en el caso del ordenador, merosnumeros posiciones de memoria. Sin embargo, el intercambio de contenidos entre dos direcciones,por ejemplo, debe hacerse sabiendo que los contenidos de aquellas direcciones son compatibles, delmismo tipo.

Ademas veremos en alguna ocasion la utilidad de poder hacer aritmetica sencilla con lospunteros: sumarle/restarle un numero entero. Al estar el puntero ligado a un tipo concreto dedatos, este aumento/disminucion de la posicion se hara correctamente: el valor del puntero a un

1decimos “se arriesgan” porque lenguajes mas modernos (como por ejemplo Java) evitan el uso de punterospor muchos motivos, entre ellos, el de que el uso de punteros es bastante difıcil de proteger, de manera que y elprogramador puede facilmente, con pequenos errores destruir todo el estado de un sistema o acceder a cualquierparte protegida del mismo.

Page 3: Memoria Dinamica - Lenguaje C

2.1 Gestion dinamica de la memoria 3

entero aumentado en uno se incrementara en realidad en el tamano de un entero apuntando alsiguiente entero (ver Fig. 3).

p

*p 333FA003FA043FA08

00000

3FA04

321*(p+1)

Figura 3: Aritmetica de punteros. Si sumamos un entero a un puntero la nueva direcciontiene en cuenta el tamano del tipo de elemento apuntado.

Una vez creada una variable o tipo puntero se inicializara, se le daran valores:

punt = 0;punt = &i;punt = &ficha;

Un valor muy especial en los punteros es el valor 0 (NULL). El valor 0 en los punteros es la indicacionde que no se apunta a nada. Aunque la direccion de memoria 0 existe, no se usa y en punteroses la indicacion basica de que no hay puntero. En los diagramas un puntero NULL se suele dibujarcomo un enlace a tierra de electricidad. Ver Figura 4

0p

Figura 4: Puntero NULL: enlace a tierra, no apunta a nada.

Hemos usado en el ejemplo anterior el operador &. El operador & es un potente operador deC/C++ que sirve para obtener la direccion de memoria de cualquier elemento.

Por ejemplo, en C/C++:

int x;int *p = &x;

significa que a la vez que creamos la variable puntero p, que es un puntero a entero, le podemosasignar la direccion donde el compilador ha situado a otra variable, tambien de tipo entero.

El tener guardada la direccion serıa algo bastante inutil si tan solo fuese eso, una direccionque se guarda, la segunda parte es acceder a esa direccion. Esto es, nos interesa conocer que es loque hay en el sitio al que apunta un puntero. Si no tuviesemos esta posibilidad, tan solo habrıamosguardado la direccion para nada.

Para acceder a lo apuntado por una direccion, por un puntero, debemos desreferenciarlo oindireccionarlo leer lo apuntado por el. Para acceder a lo apuntado por un puntero los lenguajesde programacion disponen de un operador de desreferenciacion, poniendo el operador delante delpuntero se entiende que lo que queremos es la informacion que hay en la direccion escrita en esepuntero. En C/C++, siguiendo el ejemplo anterior,

*p = x;

Page 4: Memoria Dinamica - Lenguaje C

2.2 Punteros en pseudocodigo 4

copia el contenido de la variable x al lugar (de tipo entero) al que apunta la variable puentero p.Naturalmente podemos escribir el contenido de la variable x de la forma mas sencilla como siempre

x = 3;

El resultado es equivalente en estos casos.

2.2. Punteros en pseudocodigo

Puntero

<TipoApuntado> *PTI;// NODO *ENLACE, *LINK;// N *arrPunt[1..10]; // array de 10 punteros (raro)

NOTA: en pseudolenguaje, el contenido de las estructuras se puede copiar, sin embargo no losarrays.

Ejemplos:

N *puntero_a_natural; //tipo punteroN array_natural[1..20]; //array de 20 naturales enteros

2.2.1. Utilidad de los punteros

¿Que utilidad puede tener el anterior uso de las variables de tipo puntero?Hasta ahora la introduccion de la direccion de las variables y zonas de memoria con los

punteros no anade ninguna ventaja, excepto en el lenguaje C, donde es el unico mecanismo posiblepara poder modificar parametros: pasar una copia de la direccion a la funcion y en esta indireccionary modificar el elemento pasado a traves de su direccion.

Un uso fundamental es el que se da en los lenguajes en los que no existe un paso de parametrospor referencia, como le ocurre, por ejemplo al lenguaje C, se recurre al siguiente mecanismo: a lafuncion/procedimiento se le pasa una copia (siempre) de la direccion de la variable (&) y en lafuncion se opera con lo apuntado por esa direccion (*): de esa forma se accede al contenido de lavariable que se haya pasado. Este mecanismo ya es conocido por nosotros.

Aparte de este modo de uso el conocer las direcciones de las variables estaticas de los progra-mas no tiene ningun otro interes ya que para modificarlas, leerlas, etc, bastara como siempre conusar su nombre, la etiqueta que los compiladores permiten crear para cada variable estatica:

x = 3;

Sin embargo, la segunda forma de obtener direcciones de memoria es la mas interesante y laque nos ocupara en este tema.

2.2.2. Solicitud dinamica de memoria

El principal uso de los punteros esta en liberar la rigidez de los espacios de memoria paradatos. Las variables estaticas no pueden crecer durante la ejecucion de un programa y, si nos hacefalta mas espacio para guardar mas datos durante la ejecucion de un programa, necesitaremosalgo nuevo. Los lenguajes que utilizan punteros aportan funciones que localizan y reservan paranuestro programa tamanos solicitados de memoria interna.

Para ello los programas tienen una zona de memoria libre propia (aunque tambien en lossistemas mas modernos esta memoria se pide a la del sistema, que ademas es paginable y compar-tible con memoria virtual de disco) llamada arena situada entre la pila y los datos estaticos. VerFigura 5

En C el mecanismo que se utiliza es el de decir la cantidad de memoria que se necesita a unafuncion y esta devuelve una direccion, la direccion del comienzo del lugar donde ha reservado ese

Page 5: Memoria Dinamica - Lenguaje C

2.2 Punteros en pseudocodigo 5

código

datos inicializados

datos no inicializados

pila de programa

arena

PRO

GRAM

A

Figura 5: Zona de memoria “libre” de un programa, que sirve para ser usada y reusadadinamicamente durante su ejecucion. Otras zonas son las de datos iniciali-zados: constantes y cadenas de mensajes, etc. . . y la zona que durante laejecucion y de manera estatica tienen reservadas las variables. La pila es lazona, dinamica tambien, que se usa para el paso de parametros a las funciones.

espacio de memoria2.Para ello el lenguaje C aporta basicamente dos operadores para la gestion dinamica de la

memoria: malloc y free. La primera es la que hace las reservas, la segunda es la que las libera,las devuelve al sistema3. Por ejemplo, si queremos que el sistema localice espacio para un numerode tipo entero, pedimos espacio para un numero entero. sizeof(int) es una funcion interna de Cque dice el espacio en bytes que ocupa cualquier tipo. Por otro lado la funcion malloc(..) solorequiere conocer ese dato, la cantidad de bytes que se necesitan. malloc busca espacio libre, losbloquea, y devuelve la direccion donde lo ha encontrado:

p = malloc(sizeof(int));

p = malloc(10000);

reservarıa 10.000 bytes de memoria que comenzarıan en p.

Si malloc no encuentra mas memoria libre devuelve una direccion nula: nul o NIL o 0.

Una vez que hayamos operado con la direccion (con el contenido al que apunta esa direccion,en realidad), debemos liberarla. La liberacion de los bloques pedidos al sistema es esencial si elprograma ha de seguir iterando y pidiendo en otros sitios nuevos bloques. No siempre es ası y elsistema usualmente libera todo lo prestado al programa al terminar. Pero en general es una tacticamuy sana el liberar los bloques de memoria que ya dejan de ser necesarios. La funcion en C pararealizar esto es free. free solo requiere conocer el lugar donde comienza el bloque antes prestado.Curiosamente no requiere que se le indique cuanto ocupaba aquella reserva:

free(p);

vuelve a dejar ese sitio disponible para futuras necesidades de meroria.

2.2.3. C++

C++ utiliza un operadores mas complejos “new” que deduce el tamano necesitado y modificael parametro puntero que se le pone. y “delete” para liberar esta memoria.

Estos operadores son los mismos que se utilizan para crear objetos dinamicamente y permitenademas la inicializacion de los elementos: Por ejemplo

2dado que los punteros llevan asociados el tipo de datos al que apuntan, en muchos lenguajes no es necesario decirla cantidad de memoria que queremos reservar sino solo para que tipo de dato queremos apuntar, y el compiladordeduce el espacio de memoria que se necesita y asigna esa direccion al puntero.

3Para ver las demas funciones que aporta el sistema, ver la cabecera: <stdlib.h>.

Page 6: Memoria Dinamica - Lenguaje C

2.3 Estructuras de datos recursivas 6

int *pint, *parr;pint = new int;parr = new int[10]// ...delete pint;delete[] parr;

Notese que en C++ la solicitud es siempre por unidades logicas: se pide un entero, una ficha, uncaracter; o bien se pide un array de n elementos. En el ultimo caso se tiene que liberar con lasintaxis especial delete[] loquesea;

En C++ el operador new puede llevar despues del tipo que crea un valor para inicializarlo:

double *p;p = new double (3.1416);

2.2.4. Arrays dinamicos

Una inmediata ventaja de la creacion de elementos dinamicamente es la posibilidad de creararrays del tamano que nos haga falta durante la ejecucion del programa:

int n;cout << "introducir numero de elementos: ";cin >> n;

double *datos;datos = new double[n]; // quedan inicializados a 0for (int i=0; i<n;i++)

cout << datos[i] << ", ";cout << endl;

En todos los casos, cuando sepamos que no se van a utilizar mas estas estructuras debemosliberarlas:

delete[] datos;

2.3. Estructuras de datos recursivas

Aunque se pueda localizar un bloque de memoria, el problema es que no se sabe cuantos deesos bloques de memoria necesitaremos.

Si un programa requiere diez bloques de memoria en una ejecucion habrıa que tener previstasdiez variables de tipo puntero para guardar las direcciones de estos bloques. Pero si durante laejecucion se hubiesen requerido veinte, tendrıamos un problema.

La solucion esta en a enganchar un bloque con el siguiente (nuevamente mediante punteros) yformar ası cadenas de bloques todos ligados y acceder a ellos mediante una direccion, un puntero,el puntero al primer bloque. Para poder formar estas cadenas de bloques lo que se hace es definiruna estructura de datos como (en C++) (ver Fig 6):

struct TNodo {int dato;TNodo *sigui;

};

o, para facilitar su uso,

typedef TNodo *TEnlace;struct TNodo {int dato;TEnlace sigui;

};

Page 7: Memoria Dinamica - Lenguaje C

2.4 Listas lineales de de nodos con simple y doble enlace, cabeceras 7

Es la primera vez que estamos definiendo una variable (un tipo mas bien) o un objeto cualquiera enfuncion de otro aun no definido. Esto es necesario ası por la propia recursividad de la estructura.Un enlace es un puntero a algo que tiene un enlace a lo que estamos aun definiendo.

lista

Lista

Figura 6: Lista simple: tenemos un puntero lista que apunta a un nodo el cual tieneun puntero que apunta al siguiente, etcetera.

La cuestion ahora es que cada vez que se pide un bloque para un nodo tambien se esta ga-nando espacio para guardar la direccion del siguiente nodo o bloque. De esta forma, de un solopuntero podemos colgar la cantidad de nodos que nos vayan haciendo falta durante el programa.Dependiendo del criterio utilizado para enganchar esos nodos tendremos estructuras lineales o nolineales mas o menos complejas pero que permitiran acceder a los nodos segun nos interese.

Cuando se acaba la secuencia de nodos y ya no se tienen mas nodos detras, se utiliza comoindicador de siguiente el valor de puntero nulo. De otra manera, el ultimo no tiene a nadie detrasy su siguiente es NULL.

Se deja como ejercicio la construccion de una estructura lineal o lista de nodos enlazadosa los que se les vaya anadiendo los nodos al principio. Se puede, en el ejemplo, ir anadiendoentradas a una agenda personal (nombre, telefono, email), segun el usuario vaya introduciendointeractivamente los datos en el programa.

Algo mas difıcil es anadirlos al final de todos ellos, ya para acceder al final de ellos es necesariorecorrer toda la cadena.

Se deja asımismo como ejercicio el borrar un nodo segun su posicion en la lista. Por ejemplo,borrar el nodo 5 o el 200. Tambien debemos ver como se borrarıa un nodo segun el valor del datoque contenga.

lista

nuevo

1

2

3

new

Figura 7: Insercion en cabeza; son tres pasos: obtener la memoria, apuntar al antiguoprimero y que el antiguo primero apunte al nuevo primero.

2.4. Listas lineales de de nodos con simple y doble enlace, cabeceras

De la gestion de memoria dinamica mediante listas de nodos enlazados se deduce que la mayordificultad de las estructuras de listas lineales es la de que el acceso a los nodos se hace linealmentemas y mas lento conforme aumenta la longitud de la lista. Es por eso que se desarrollan estructuraspara tratar de evitar estos retrasos y tratar de igualar los tiempos de acceso a todos los nodos dela lista.

Page 8: Memoria Dinamica - Lenguaje C

2.4 Listas lineales de de nodos con simple y doble enlace, cabeceras 8

lista

nuevo

1

34

new

tmp 2

Figura 8: Insercion en mitad o final; son cuatro pasos: obtener la memoria, avanzarun puntero al anterior al punto de insercion, apuntar al siguiente y que elanterior apunte al nuevo.

lista

aborrar 1

23

Figura 9: Borrar el primero; son tres pasos: guardar su direccion, apuntar con lalista al siguiente (aquı hay que modificar la variable de la lista), liberar el quehay que borrar.

Las cabeceras son estructuras de datos que contienen informacion relevante acerca de la cadenade nodos enlazados. Esta informacion usualmente contiene el total de nodos existentes, quizas laposicion del ultimo nodo (para facilitar la operacion de anadido por la cola, que puede ser frecuente,segun el caso).

La cabecera siempre tiene que guardar la posicion del primer nodo de la cadena, pero ademaspuede ser realmente util guardar la posicion y el numero del nodo (posicion en la cadena) delque se accedio la ultima vez: la ultima visita. Y es que lo mas usual en estructuras de datos,especialmente en las lineales, en las cadenas de nodos dinamicamente enlazados, es la operacionderecorrido secuencial (por ejemplo, buscando, imprimiendo, etc.), de manera que si guardamos laposicion de la ultima visita, cuando nos pidan la siguiente posicion el acceso sera inmediato, decomplejidad 1: mirar vel el siguiente nodo del de la ultima visita y poner el valor de la ultimavisita en eese siguiente nodo antes de volver.

El doble enlace mejora el acceso aleatorio a posiciones cualesquiera de la lista en la que yase recuerde la posicion de la ultima visita. Si guardamos la posicion y direccion del ultimo nodovisitado, solo tendremos optimizado el acceso secuencial (hacia adelante) de los nodos de la lista.En estos casos se consigue tambien una gran mejora si podemos acceder a los nodos no solo haciaadelante sino tambien a los nodos que estan antes que el nuestro, apuntando ası no solo a “sigui”sino tambien a “anterior”.

Page 9: Memoria Dinamica - Lenguaje C

2.5 Listas posicionales y ordenadas 9

lista

aborrar1

3

tmp2

4

Figura 10: Borrar en medio o al final; son cuatro pasos: avanzar hasta el anterioral que haya que borrar, guardar la direccion del nodo a eliminar, hacer queel anterior apunte al siguiente, eliminar.

lista

Figura 11: Lista con una estructura de cabecera donde guardar metadatos im-portantes de la lista.

2.5. Listas posicionales y ordenadas

El acceso a los nodos de las listas se suele hacer por la posicion, esto es, dada una posicion,desde 1 hasta la longitud o total de elementos de la lista, se devuelve el puntero o la informacioncontenida en ese nodo. Pero tambien es muy importante el caso en el que se guardan los nodosordenadamente. En este caso el acceso posterior puede ser mucho mas eficiente. Sin embargoel costo inicial es mayor. Ası que depende de cual vaya a ser la operacion mas frecuente, lade anadidos/borrados o la de busqueda, porsterior, para que nos interese mantener a los nodosordenados o no.

Page 10: Memoria Dinamica - Lenguaje C

2.5 Listas posicionales y ordenadas 10

lista

p ultima visitanultvis

Figura 12: Lista con dobles enlaces y una cabecera para aprovecharlos manteniendorecuerdo de la ultima visita

Juan FalguerasDpto. Lenguajes y Ciencias de la Computacion

Universidad de MalagaDespacho 3.2.32