Está en la página 1de 10

Asignacion de Memoria (Allocation)

I Estatica (o en tiempo de compilacion)


I El compilador asigna la memoria necesaria para almacenar los
Algoritmos y Estructuras de Datos I valores asociados a cada variable del programa.
I El espacio de memoria requerido para almacenar los valores del
tipo se conoce en tiempo de compilacion.
Segundo cuatrimestre de 2014 I Dinamica (o en tiempo de ejecucion)
Departamento de Computacion - FCEyN - UBA
I La memoria se asigna durante la ejecucion del programa.
I El espacio de memoria asignado a un proceso varia durante la
ejecucion.
Programacion imperativa - clase 14 I Los valores se alojan en una parte de la RAM reservada al
proceso denominada memoria dinamica (o heap).
Memoria dinamica y listas enlazadas

1 2

Asignacion de Dinamica de Memoria Accesos a la memoria dinamica (desrefenciacion)


I Para asignar dinamicamente espacio de memoria utilizamos el I Para acceder al valor contenido en la memoria apuntada por
operador new, seguido por el tipo de valores a almacenar un puntero, debemos desreferenciar: acceder a la posicion de
new int //asigna dinamicamente memoria para almacenar un int memoria apuntada por el puntero
new float // asigna dinamicamente memoria para almacenar un double I El operador de desreferenciacion es *
I el operador new retorna la direccion de memoria donde int* p = new int; //declara y asigna dinamicamente memoria para el
puntero p
comienza el espacio asignado. *p = 5; //asigna a la memoria apuntada por p el valor 5
I El tipo puntero a(*). cout << *p; //muestra el valor 5
I Por ejemplo: Notar diferencia con cout<<p que muestra una direccion de
int* p; //declara a la variable p como un puntero a un entero memoria.
I Los valores de p son direcciones de memoria que almacenan I Las siguiente asignaciones son incorrectas
enteros. int a;
int* p;
int* p; //declara a la variable p como un puntero a un entero
....
p = new int //asigna memoria para almacenar un int
p = 10; //MAL: p es de tipo puntero a entero, no un entero
//y guarda su direccion en p.
p = a; //MAL: p es de tipo puntero a entero, no un entero
a = p //MAL: a es de tipo entero, no un puntero a entero

3 4
Asignacion de punteros (Aliasing) Puntero a NULL
I Consideremos este codigo: I Es un valor especial que indica que el puntero no
int* a = new int; esta apuntando a ninguna posicion de memoria.
*a = 10;
int* b;
I Se indica por la constante 0 (o una expresion que evalue 0).
b = a; // a y b apuntan a la misma direccion de memoria I Puede usarse la macro NULL que esta definida como el valor 0.
cout<<*b; // Muestra 10
*b = 5; int* a = new int;
cout<<*a //Muestra 5; ...
a = 0; // a no apunta a ninguna posicion de memoria
I Esto se debe a que las dos variables apuntan a la misma
posicion de memoria.
I Si se desreferencia el puntero nulo se produce un error en
tiempo de ejecucion (Segmentation fault).
I El mismo fenomeno que ocurre cuando utilizamos referencias.
int* a = new int;
...
a = NULL; // a no apunta a ninguna posicion de memoria
cout<<*a; //Error: Se desreferencia al puntero a NULL

5 6

Alcance de las variables Tiempo de vida de un objeto


I Alcance: Es la porcion de codigo en donde vale la asociacion I Tiempo de vida de un objeto es el lapso de ejecucion del
entre el nombre de la variable y la posicion de memoria que programa en el que una porcion de memoria esta reservada
designa la variable (es decir, donde es correcto usar el nombre para almacenar a dicho objeto.
de la variable para referirse a la posicion de memoria). I Para las variables locales, el tiempo de vida coincide con la
I El alcance de las variables locales en C++ esta designado por ejecucion de su alcance.
un par de llaves {...}. I Para los objetos creados dinamicamente, el tiempo de vida no
int f(){
esta determinado estaticamente.

int n = 5;
return n; Alcance de n int* f () { 
} int* n = new int;
return n; Alcance de n
}
La memoria reservada con la operacion new int no se libera al
finalizar la ejecucion del alcance en la que fue asignada.

7 8
Perdida de memoria (Memory Leaks) Referencias colgantes (Dangling references)
I Uso incorrecto de memoria I Luego de ejecutar delete a la memoria apuntada por a no
I Se asigno dinamicamente memoria que no puede accederse debera ser utilizada, sin embargo C++ no lo prohibe y es
mas, porque se pierden las referencias. responsabilidad del programador.
int* a = new int;
I El siguiente codigo no produce error, pero accede mal a la
a = 0; // se pierde la direccion de memoria asignada memoria.
... // int* a = new int;
*a = 5;
I La memoria asignada con new permanece inutilizable y debe delete a;
cout<<*a; //Mal uso de memoria: acceso a memoria que ha sido liberada
ser liberada
I Para ello usamos el operador delete. I Algunos programadores utilizan la siguiente convencion.
int* a = new int;
Asignar el puntero a NULL luego de liberar
... int* a = new int;
delete a; // la memoria apuntada por a es liberada *a = 5;
delete a;
a = 0;
I Para no perder memoria, cada porcion de memoria que se cout<<*a; //Error en tiempo de ejecucion
asigna con new, debe liberarse con delete.
I Se resuelve el problema?
I No, en presencia de aliasing.
9 10

Referencias colgantes (Dangling references) Referencias a instancias de clases


I Se resuelve el problema? No, en presencia de aliasing pueden I Considerar
quedar referencias colgantes class Complex{
public:
int* a, *b;
Complex(float r, float i);
...
Complex suma(Complex1 c);
b = a;
Complex resta(Complex1 c);
...
void show();
delete a;
a = 0;
private:
cout<<*b; //Acceso incorrecto a memoria liberada
float real, imag; };

I Es responsabilidad del programador administrar el uso de la I int main(){


memoria. Complex a = Complex(1,2);
Complex* b = new Complex(10,4);
I Dado que esto se considera una actividad de bajo nivel de a.show()
(*b).show();
abstraccion, muchos lenguajes modernos liberan al b -> show(); }
programador de esta actividad
I Incorporando un recolector de basura (garbage collector).
I El operador -> es equivalente a dereferenciar y seleccionar una
operacion o un atributo.
I this es un puntero.
11 12
Arreglos Listas enlazadas

Podemos pedir un arreglo de forma dinamica? Queremos una estructura para guardar datos, de largo variable.
tipo Lista<T> {
I S. Usando new[exp]. observador elems(l:Lista<T>): [T];
Pero la longitud queda fija para siempre. }
I Tenemos que ocuparnos de liberar la memoria usando problema agregarAdelante(l:Lista<T>, e: T) {
delete[] modifica l;
asegura elems(l)==e:elems(pre(l));
int n = 5;
int[] a = new a[n];
}
... problema agregarAtras(l:Lista<T>, e: T) {
delete a[]; modifica l;
asegura elems(l)==elems(pre(l))++[e];
}
problema quitarPrimero(l:Lista<T>) {
requiere |elems(l)| > 0;
modifica l;
asegura elems(l)==elems(pre(l))[1..|elems(pre(l)|)];
}

13 14

Listas enlazadas Listas enlazadas

Queremos una estructura para guardar datos, de largo variable. I Por ejemplo, creamos tres nodos de la siguiente forma
I Idea: Pedir memoria para un elemento por vez y guardar una (asumiendo elemento es int):
referencia a este elemento. Nodo* nodo1 = new Nodo;
I Donde? nodo1 -> elemento = 1;
Nodo* nodo2 = new Nodo;
En un elemento anterior! nodo2 -> elemento = 2;
I Creamos una estructura de datos Nodo que guarda el Nodo* nodo3 = new Nodo;
nodo3 -> elemento = 3;
elemento que queremos almacenar y un puntero a otro Nodo:
struct Nodo { I Graficamente:
T elemento;
nodo1 nodo2 nodo3
Nodo* siguiente;
}

I La clausula struct permite definir estructuras agrupando elemento 1 elemento 2 elemento 3


diferentes componentes basicos. siguiente siguiente siguiente
I Es similar a class pero, al contrario que class, brinda por
defecto accesibilidad publica a sus elementos. A veces se usa
para definir estructuras de datos dentro de clases
rerpesentando TADs.
15 16
Listas enlazadas Listas enlazadas
I Luego, hacemos que el nodo1 referencie al nodo2 y el nodo2 I Notemos entonces que con solo mantener una referencia al
al nodo3. El nodo3 referencia a null para indicar que es el final primer nodo de la lista, podemos acceder a todos los
de la lista: elementos:
prim
nodo1 -> siguiente = nodo2;
nodo2 -> siguiente = nodo3;
nodo3 -> siguiente = NULL;
elemento 1 elemento 2 elemento 3
I Graficamente: siguiente siguiente siguiente
nodo1 nodo2 nodo3

elemento 1 elemento 2 elemento 3


siguiente siguiente siguiente

17 18

Listas enlazadas Listas enlazadas


I Aprovechando esto, queremos construir una Lista que I Solo mantenemos una referencia al primer nodo, con lo cual
responda a las siguientes operaciones: una Lista va a estar representada solo por esta variable:
Lista<int> l; template <class T>
l.imprimir(); //imprime [] class Lista {
l.agregarAdelante(2); struct Nodo {
l.imprimir(); //imprime [2] T elemento;
l.agregarAdelante(4); Nodo* siguiente;
l.imprimir(); //imprime [4 2] };
l.agregarAdelante(2); Nodo* primero;
l.imprimir(); //imprime [2 4 2] };
l.agregarAdelante(8);
l.imprimir(); //imprime [8 2 4 2] I La lista vaca es aquella que no tiene un primer nodo, o dicho
de otra manera, que el nodo primero vale NULL.
I Todo nodo referencia al siguiente nodo en la lista. Si es el
ultimo no referencia a ninguno, es decir, su variable
siguiente vale NULL.

19 20
Constructores y Destructores Metodo imprimir
I Constructor: Permite definir el valor inicial de una lista recien I Para imprimir una lista, nos paramos en el primer elemento y
creada (estatica o dinamicamente). Garantiza que se cumpla vamos recorriendo la lista utilizando la referencia al siguiente:
el invariante de representacion! class Lista<T> {
public:
class Lista<T> {
void imprimir() {
Lista() {
cout<<"[";
primero = NULL;
Nodo* n = this -> primero;
}
while (n != NULL) {
private:
cout<<n -> elemento<<" ";
...
n = n -> siguiente;
Nodo* primero;
}
}
cout<<"]"<<endl;
}
I Destructor: Garantiza que cuando una instancia es destruida, }
se limpie tambien la memoria correspondiente a las instancias
creadas o que dependen de ella.
class Lista<T> {
~Lista() { ... // Luego volvemos a esto }
}

21 22

Metodo agregarAdelante Esquemas de iteracion: arreglo


I Como agregamos un elemento al principio de la lista? I Un esquema de iteracion es la estructura algortmica para
I Necesitamos crear un nuevo Nodo y que este pase a formar recorrer los elementos de una estructura de datos.
parte de la lista. I En un arreglo, recorremos los elementos con un ciclo con un
I Para agregarlo adelante, debe pasar a ser el primer nodo de la ndice:
lista, y debemos tener cuidado de que su variable siguiente I int i=0;
apunte al nodo apropiado. while( i < n )
{
I void agregarAdelante(T x) // hacer algo con arr[i]
{ ++i;
Nodo* nuevo = new Nodo; }
nuevo -> elemento = x;
nuevo -> siguiente = this -> primero;
this -> primero = nuevo;
}

23 24
Esquemas de iteracion: lista Metodo largo
I En la lista, tenemos que crear una referencia a nodo, que se I Ejercicio: escribir un metodo int largo() que devuelve el largo
inicializa con el primer nodo de la lista y en cada paso se de la lista.
actualiza con el siguiente nodo: I int largo()
I Nodo* n = this -> primero; {
int l=0;
while (n != NULL)
Nodo* n = this->primero;
{
while (n != NULL)
// hacer algo con n.elemento
{
n = n -> siguiente
l++;
}
n = n -> siguiente;
}
I Este algoritmo puede alterarse para frenar en el ultimo return l;
elemento o en un elemento buscado. }

25 26

Metodo agregarAtras Metodo agregarAtras


I Siguiendo con la lista del primer ejemplo, incluyamos el I Ejercicio: codificar la funcion agregarAtras
siguiente metodo: I Solucion:
l.imprimir(); //imprime [8 2 4 2] void agregarAtras(T x)
l.agregarAtras(16); {
l.imprimir(); //imprime [8 2 4 2 16] Nodo* nuevo = new Nodo;
l.agregarAtras(32); nuevo -> elemento = x;
l.imprimir(); //imprime [8 2 4 2 16 32] if(this -> primero==NULL) // Esta vacia?
this -> primero = nuevo;
else { // Avanzar hasta el ultimo
Nodo* n = this -> primero;
while(n -> siguiente!=NULL)
n = n -> siguiente;
n -> siguiente = nuevo;
}
}

27 28
Metodo quitarPrimero Destructor
I Eliminar el primer elemento de la lista I El destructor se ocupa de limpiar lo hecho por la instrancia
l.imprimir(); //imprime [8 2 4 2 16 32] destruida.
l.quitarPrimero();
l.imprimir(); //imprime [2 4 2 15 32]
I En particular debe liberar la memoria ocupada por las
instancias que pudo haber creado.
I Tenemos que asegurarnos de liberar le memoria del elemento I en el caso de la Lista enlazada se debe ocupar la memoria
eliminado ocupada por cada nodo.
void quitarPrimero() { class Lista<T> {
Nodo * n = this -> primero; ~Lista() {
// El nuevo primero es el siguiente Nodo* n = this -> primero;
this -> primero = this -> primero -> siguiente; while(n!=NULL) {
// Libero la memoria Nodo* sig = n -> siguiente;
delete n; delete n;
} n=sig;
}
}

29 30

Listas enlazadas - Ampliando la clase (1) Listas enlazadas - Ampliando la clase (1)
I Que orden de complejidad tiene la funcion largo()? I Cambia el constructor ...
I Ampliamos la clase agregando una variable de instancia con la Lista()
{
cantidad de nodos de la lista: primero = NULL;
template <class T> nodos = 0;
class Lista<T> }
{
...
Nodo* primero;
int nodos;
}

I Cual sera el invariante de representacion? y el abs?


I Tenemos que actualizar el codigo de los metodos que
actualizan la lista!

31 32
Listas enlazadas - Ampliando la clase (1) Listas enlazadas - Ampliando la clase (1)
I Cambia el metodo agregarAdelante ... I Cual es la ventaja de haber agregado esta variable?
void agregarAdelante(int x) I Ahora el metodo largo es mas eficiente!
{
Nodo* nuevo = new Nodo; int largo()
nuevo -> elemento = x; {
nuevo -> siguiente = this -> primero; return nodos;
this -> primero = nuevo; }
++nodos;
} I En la version anterior, este metodo tena complejidad O(n), y
I ... y todos los metodos que alteren los nodos de la lista! la version actual tieme complejidad O(1).
I Mejoramos la eficiencia de uno de los metodos, sin empeorar el
orden del resto (solo agregamos una instruccion a cada uno).

33 34

Listas enlazadas - Ampliando la clase (2) Listas enlazadas - Ampliando la clase (2)
I Como es el algoritmo para agregar un elemento al final de la I Una instancia de ListaInt tiene ahora dos referencias:
lista? (lo llamamos agregarAtras).
I La representacion de listas con una referencia al primer
elemento genera una asimetra entre los metodos
agregarAdelante y agregarAtras.
I Agregar al principio es O(1).
I Agregar al final es O(n), porque tenemos que recorrer toda la
lista para localizar el ultimo nodo.

I Solucion: Mantener una referencia al ultimo nodo!


class Lista<T>
{ I Ahora, el metodo agregarAtras no necesita recorrer la lista
Nodo* primero; para ubicar el ultimo nodo, y entonces pasa a ser O(1).
Nodo* ultimo;
} I Tenemos que actualizar los metodos que modifican la lista.

35 36
Listas doblemente enlazadas
I Cada nodo tiene una referencia al siguiente elemento, y
tambien una referencia al elemento anterior.

37

También podría gustarte