Apuntes y ejercicios de gestión dinámica de memoria

Anuncio
ETSI Telecomunicación
Memoria Dinámica
Tema
2. Memoria Dinámica
Contenido
§
Gestión de Memoria Dinámica
− Introducción. Datos estáticos y dinámicos
− Asignación dinámica de memoria
§
Tipo Puntero
− Declaración de variables de tipo puntero
− Operaciones con punteros
− Gestión dinámica de memoria
− Punteros a registros
§
Operaciones sobre Listas Enlazadas
− Otras clases de listas enlazadas
§
Ejemplo de Gestión de Memoria Dinámica en C/C++
§
Ejercicios Propuestos
§
Bibliografía
Elementos de Programación
Página 1
ETSI Telecomunicación
Memoria Dinámica
2.1. Gestión Dinámica de Memoria
En este apartado se analizarán las causas que llevan a los lenguajes de programación a
ofrecer elementos y características necesarias para permitir la gestión de memoria de manera
dinámica. Se verán cuáles son las ventajas e inconvenientes de esta técnica, así como las
diferencias entre tipos de datos estáticos y dinámicos en cuanto a su almacenamiento en
memoria principal y su tratamiento por parte de los programas (algoritmos) que los manejan.
Así mismo, se establecerán las bases teóricas necesarias para la comprensión del
funcionamiento del mecanismo de gestión dinámica de memoria.
2.1.1. Introducción
Los tipos de datos, tanto simples como estructurados, vistos hasta ahora en los temas
anteriores de las asignaturas de Introducción a los Computadores (IC) y Elementos de
Programación (EP), sirven para describir datos o estructuras de datos cuyos tamaños y formas
se conocen de antemano. Sin embargo, en muchos programas es necesario que las
estructuras de datos estén diseñadas de manera que su tamaño y forma varíe a lo largo de la
ejecución de aquellos. Con esto se consigue, fundamentalmente, que estos programas
funcionen de manera más eficiente y con un aprovechamiento óptimo de los recursos de
almacenamiento en memoria principal.
Las variables de todos los tipos de datos vistos hasta el momento son denominadas
variables estáticas, en el sentido en que se declaran en el programa, se designan por medio
del identificador declarado, y se reserva para ellas un espacio en memoria en tiempo de
compilación de los programas. El contenido de la variable estática puede cambiar durante la
ejecución del programa o subprograma donde está declarada, pero no así el tamaño en
memoria reservado para ella. Esto significa que la dimensión de las estructuras de datos a las
que se refieren estas variables debe estar determinada en tiempo de compilación, lo que puede
suponer una gestión ineficiente de la memoria, en el sentido de que puede implicar el
desperdicio (por sobredimensionamiento) o la insuficiencia (por infradimensionamiento) de
memoria.
Sin embargo, son muchos los lenguajes de programación que ofrecen la posibilidad de
crear y destruir variables en tiempo de ejecución, de manera dinámica, a medida que van
siendo necesitadas durante la ejecución del programa. Puesto que estas variables no son
declaradas explícitamente en el programa y no tienen identificador (nombre) asignado, se
denominan variables anónimas. El pseudolenguaje utilizado en las asignaturas de IC y EP
permite el uso de este tipo de variables. Para ello, ofrece los mecanismos y la sintaxis
necesaria para su creación, a la vez que proporcionará una manera de referirse a estas
Elementos de Programación
Página 2
ETSI Telecomunicación
Memoria Dinámica
variables para el acceso a los datos que contienen y la asignación de valores a los mismos.
Todo esto se lleva a cabo mediante el empleo del tipo puntero, cuyas características se
expondrán en los siguientes apartados.
Datos estáticos: su tamaño y forma es constante durante la ejecución de un programa y, por
tanto, se determinan en tiempo de compilación. El ejemplo típico son los arrays. Tienen el
problema de que hay que dimensionar la estructura de antemano, lo que puede conllevar
desperdicio o falta de memoria.
Datos dinámicos: su tamaño y forma es variable (o puede serlo) a lo largo de un programa,
por lo que se crean y destruyen en tiempo de ejecución. Esto permite dimensionar la estructura
de datos de una forma precisa: se va asignando memoria en tiempo de ejecución según se va
necesitando.
2.1.2. Asignación dinámica de memoria
Cuando se habla de asignación dinámica de memoria se hace referencia al hecho de crear
variables anónimas −es decir, reservar espacio en memoria para estas variables en tiempo de
ejecución del programa− así como liberar el espacio reservado para dichas variables anónimas,
cuando ya no son necesarias, también durante el tiempo de ejecución.
Instrucciones de
programa
Datos estáticos
Límite de datos
estáticos
Zona dinámica
fragmentada
(heap)
Límite de la pila
Pila
Puntero de la pila
(stack pointer)
Figura 2.1. Esquema de asignación de memoria
Elementos de Programación
Página 3
ETSI Telecomunicación
Memoria Dinámica
La zona de la memoria principal del computador donde se reservan espacios para
asignarlos a variables dinámicas se denomina heap o montón. Cuando el sistema operativo
carga un programa para ejecutarlo y lo convierte en proceso, le asigna cuatro partes lógicas en
memoria principal: instrucciones, datos (estáticos), pila y una zona libre. Esta zona libre (heap)
es la que va a contener los datos dinámicos. En cada instante de la ejecución del programa, el
heap tendrá partes asignadas a datos dinámicos y partes libres disponibles para asignación de
memoria, como puede observarse en la figura 2.1. El mecanismo de asignación-liberación de
memoria durante la ejecución del programa hace que esta zona esté usualmente fragmentada
(ver figura 2.1), siendo posible que se agote su capacidad si no se liberan las partes utilizadas
ya inservibles. (La pila también varía su tamaño dinámicamente, pero la gestiona el sistema
operativo, no el programador.)
Para trabajar con datos dinámicos son necesarias dos cosas:
•
Subalgoritmos predefinidos en el lenguaje (pseudolenguaje) que permitan gestionar la
memoria de forma dinámica (asignación y liberación).
•
Algún tipo de dato con el que sea posible acceder a esos datos dinámicos (ya que con
los tipos vistos hasta ahora en las asignaturas de IC y EP sólo se puede acceder a
datos con un tamaño y forma ya determinados).
2.2. Tipo Puntero
El tipo puntero y las variables declaradas de tipo puntero se comportan de manera diferente a
las variables estáticas estudiadas en los temas anteriores de las asignaturas de IC y EP. Hasta
ahora, cuando se declaraba una variable de un determinado tipo, ésta podía contener
‘directamente’ un valor de dicho tipo, simplemente llevando a cabo una asignación de ese valor
a la variable. Con las variables de tipo puntero esto no es así.
Las variables de tipo puntero permiten referenciar datos dinámicos, es decir, estructuras de
datos cuyo tamaño varía en tiempo de ejecución. Para ello, es necesario diferenciar claramente
entre:
•
la variable referencia o apuntadora, de tipo puntero,
•
y la variable anónima referenciada o apuntada, de cualquier tipo, tipo que está
asociado siempre al puntero.
Físicamente, el puntero no es más que una dirección de memoria. En la figura 2.2 se
muestra un ejemplo, a modo de esquema teórico, de lo que podría ser el contenido de varias
posiciones de memoria principal, en la que se puede ver cómo una variable apuntadora o
puntero, almacenado en la posición de memoria 7881(16, contiene, a su vez, otra dirección de
memoria, la 78AC(16, la de la variable referenciada o anónima, que contiene el dato 6677(16.
Elementos de Programación
Página 4
ETSI Telecomunicación
Memoria Dinámica
De esta manera se ilustra cómo el puntero contiene una dirección de memoria que ‘apunta’ a la
posición de memoria donde se almacena un dato de cierto tipo asociado al puntero.
Dirección
Contenido
...
7881(16
...
78AA(16
78AB(16
78AC(16
78AD(16
78AE(16
...
Puntero
Variable
referenciada
...
78AC(16
...
AACC(16
6743(16
6677(16
89FF(16
DC34(16
...
Figura 2.2. Esquema de posiciones de memoria con punteros
Definición: un puntero es una variable cuyo valor es la dirección de memoria de otra variable
Según su definición, un puntero se ‘refiere’ indirectamente a un valor, por lo que no hay que
confundir una dirección de memoria con su contenido (ver figura 2.3).
VARIABLES
C car = ‘z’
Dirección
...
...
7C16(16
7C17(16
7C18(16
...
‘z’
...
...
...
Dirección de la variable ‘car’ = 7C17(16
Contenido de la variable ‘car’ = ‘z’
Figura 2.3. Esquema de posiciones de memoria donde se muestra la diferencia entre la
dirección de una variable y su contenido
Una variable de tipo puntero no puede apuntar a cualquier variable anónima; debe apuntar
a variables anónimas de un determinado tipo. El tipo de la variable anónima debe ser incluido
en la especificación del tipo de la variable puntero.
Elementos de Programación
Página 5
ETSI Telecomunicación
Memoria Dinámica
2.2.1. Declaración de variables de tipo puntero
La declaración de una variable de tipo puntero1 en el pseudolenguaje de la asignatura consiste
en un tipo base, un asterisco ‘*’ y el nombre de la variable. La forma general de la declaración
de una variable de tipo puntero es, según la notación BNF, la siguiente (en la correspondiente
sección de VARIABLES):
<Def_TipoPuntero> ::= <TipoBase> *<TipoPuntero>
donde <TipoBase> es el tipo base del puntero, que puede ser cualquier tipo válido, simple
o compuesto. Ejemplos de declaración de variables de tipo puntero son los siguientes:
VARIABLES
N *contador // Puntero a una variable de tipo natural (N)
C *car // Puntero a una variable de tipo carácter (C)
Así, la variable contador del ejemplo anterior no contiene un valor de tipo natural (N), sino
la dirección de memoria donde estará almacenado un valor de tipo natural. El valor
almacenado en la variable anónima de tipo natural será accesible a través del puntero
contador.
También es posible, como para el resto de los tipos simples o compuestos vistos en las
asignaturas de IC y EP, declarar nuevos tipos puntero, mediante la inclusión de los mismos en
la correspondiente sección de TIPOS del algoritmo, siguiendo la sintaxis vista más arriba. A
partir de estos nuevos tipos, pueden declararse nuevas variables. Por ejemplo,
TIPOS
N *TipoPtrNatural // Tipo puntero a un número natural
C *TipoPtrCaracter // Tipo puntero a un carácter
...
VARIABLES
TipoPtrNatural contador, ptr // Variables de tipo puntero
// a un número natural
TipoPtrCaracter car // Variable de tipo puntero a un carácter
ptr
33
Figura 2.4. Representación gráfica de punteros
1
Para abreviar, se suele llamar puntero a una variable de tipo puntero, por lo que, a partir de ahora, se utilizará más
asiduamente ese primer término por ser de uso más común y conciso.
Elementos de Programación
Página 6
ETSI Telecomunicación
Memoria Dinámica
El hecho de que la variable ptr, declarada en el ejemplo anterior, esté apuntado a un dato
de tipo natural de valor, por ejemplo, 33, puede representarse gráficamente como en la figura
2.4 (siendo muy útil este tipo de representación para posteriores operaciones donde intervienen
punteros de una manera más compleja, como en el caso de las listas enlazadas que se
analizarán al final del capítulo).
Un puntero puede apuntar a cualquier tipo de dato predefinido del pseudolenguaje o bien
definido por el usuario, tanto tipos simples como tipos compuestos. Es importante tener en
cuenta, en el caso de tipos definidos por el usuario, que primero debe declararse el tipo de
datos al que apuntará el puntero (un array, un registro, etc.) y, posteriormente, el tipo de datos
puntero a ese tipo definido por el usuario. Por ejemplo,
TIPOS
REGISTRO TipoComplejo
R parteReal, parteImaginaria
FINREGISTRO
TipoComplejo *TipoPtrComplejo /* TipoPtrComplejo es un tipo
puntero a un registro */
VARIABLES
TipoComplejo *ptr1 // Puntero a un registro
TipoPtrComplejo ptr2 // Puntero a un registro
De esta forma, en el ejemplo anterior, ptr1 y ptr2 son variables de tipo puntero que
contendrán direcciones de memoria donde estarán almacenadas variables (anónimas) de tipo
TipoComplejo. Es importante, por tanto, que el dato al que apunte el puntero sea del tipo
base del que se ha declarado éste. Puede considerarse como una buena norma de estilo, la
declaración del tipo de datos puntero a <TipoBase> inmediatamente después de la propia
declaración
de
<TipoBase>,
como
se
ha
hecho
en
el
ejemplo
anterior
para
TipoPtrComplejo y TipoComplejo, respectivamente.
2.2.2. Operaciones con Punteros
Las operaciones que se pueden llevar a cabo con punteros son:
§
Operaciones específicas de punteros.
§
Asignación de punteros.
§
Comparación de punteros.
Operadores específicos de punteros
Para trabajar con punteros se utilizan dos operadores específicos: el operador de dirección (&)
y el operador de indirección (*).
Elementos de Programación
Página 7
ETSI Telecomunicación
Memoria Dinámica
El operador de dirección (&) es un operador monario (sólo requiere un operando) que
devuelve la dirección de memoria del operando. Por ejemplo,
...
VARIABLES
Z valor, dato = -333
Z *ptrValor
...
INICIO
...
valor = 999
ptrValor = &valor
En el ejemplo anterior se consigue que el puntero ptrValor contenga la dirección de
memoria donde está almacenado el dato que contiene la variable valor, es decir, 999. Puede
decirse que la instrucción ptrValor = &valor significa ‘ptrValor recibe la dirección de
valor’. Esto puede verse gráficamente en la figura 2.5.
ptrValor = &valor
Dirección
...
...
7C16
7C17
7C18
-333
999
...
dato
valor
...
7C17
ptrValor
Figura 2.5. Operador de dirección
El operador de contenido o indirección (*) es el operador complementario del operador de
dirección (&). Es también un operador monario, que devuelve el valor de la variable anónima
ubicada en la dirección a la que apunta el puntero.
Continuando con el ejemplo anterior, si ptrValor contiene la dirección de memoria de la
variable valor, entonces es posible hacer la siguiente asignación:
dato = *ptrValor
dato = *ptrValor
Dirección
...
...
7C16
7C17
7C18
999
999
...
dato
valor
...
7C17
ptrValor
Figura 2.6. Operador de indirección
Elementos de Programación
Página 8
ETSI Telecomunicación
Memoria Dinámica
Esta asignación colocará el valor de la posición de memoria 7C17(16, es decir el número
999, en la variable dato, como se esquematiza en la figura 2.6.
Nota: no se debe confundir el operador * de las declaraciones de punteros (N *ptrNatural)
con el operador de indirección usado en los ejemplos anteriores (*ptrNatural = 939).
Asignación de punteros
Justo después de declarar un puntero con la sintaxis vista en el apartado 2.2.1, el puntero
contiene un valor indeterminado. Por ello, no es correcto, desde el punto de vista del
pseudolenguaje, hacer uso del puntero (por ejemplo a la derecha de una asignación en la que
aparece precedido del operador de indirección) antes de asignarle valor al mismo.
Una primera manera de inicializar el valor de un puntero es asignarle el valor nulo. De esta
manera podemos considerar que el puntero, en lugar de apuntar a una posición indeterminada
cuyo acceso sería incorrecto (porque podría contener cualquier dato incluso de otros
programas), no estará apuntando ‘a ninguna parte’. La manera de que dispone el
pseudolenguaje de realizar esta inicialización es mediante la asignación al puntero,
independientemente de su tipo, de la constante predefinida NULO. Podríamos considerar que el
pseudolenguje ‘garantiza que no existe ningún dato en la posición NULO’. Por ejemplo, si se
declara la variable ptrReal como un puntero a un número real (R), sería posible hacer la
siguiente asignación, indicando que ptrReal ‘no apunta a ninguna parte’ en este momento:
ptrReal = NULO
Esto se representará gráficamente como en la figura 2.7.
ptrReal = NULO
ptrReal
Figura 2.7. Asignación de NULO a una variable de tipo puntero
Es posible asignar el valor de una variable puntero a otra variable puntero, siempre que
ambas sean del mismo tipo. Por ejemplo,
R *ptrReal1, *ptrReal2=NULO
...
ptrReal1 = ptrReal2
En este último ejemplo la variable ptrReal1 apuntará a donde apunte la variable
ptrReal2, en este caso a NULO. Es importante tener en cuenta que si ptrReal1, antes de la
Elementos de Programación
Página 9
ETSI Telecomunicación
Memoria Dinámica
asignación, estaba apuntando a una variable anónima de tipo real (y, por tanto, tenía un valor
distinto del valor NULO), ésta será a partir de ahora inaccesible, puesto que la única manera de
acceder a ella era a través del puntero ptrReal1 y ahora éste apunta a otra variable anónima
o, como en este ejemplo, a NULO.
Comparación de punteros
Es posible comparar dos variables de tipo puntero en una expresión relacional usando
operadores relacionales de igualdad (==), desigualdad (!=) y comparación (<, >, <=, >=).
Dos variables puntero son iguales si ambas apuntan a la misma variable anónima o ambas
están inicializadas al valor NULO. Los punteros que constituyen los operandos de estas
operaciones relacionales binarias deben ser siempre del mismo tipo. Sin embargo, siempre es
posible comparar cualquier puntero (igualdad o desigualdad) con el valor NULO. Ejemplo,
SI (ptr1 < ptr2) ENTONCES
Escribir(“ptr1 apunta a una dirección menor que ptr2”)
FINSI
2.2.3. Gestión Dinámica de Memoria
Por gestión dinámica de memoria se entiende el hecho de crear variables anónimas, es decir,
reservar espacio en memoria para estas variables en tiempo de ejecución, y también de liberar
el espacio ocupado en memoria por una variable anónima, asimismo en tiempo de ejecución,
cuando esa variable ya no es necesaria.
Por tanto, antes de asignar a la variable anónima de un puntero un determinado valor (por
ejemplo, *ptrNatual = 333) es necesario reservar memoria para almacenar dicho valor.
Reservar memoria significa que el sistema le asigna al puntero (ptrNatural) una dirección de
memoria libre para su variable anónima donde podrá guardar el valor asignado (en este caso
333). Si la reserva de memoria no se realiza como paso previo a la asignación anterior, se
producirá una ‘violación de acceso a memoria’ porque el puntero estará inicialmente apuntando
a una dirección indeterminada o nula donde no es posible guardar el dato.
El pseudolenguaje de las asignaturas de introducción a la programación consta de dos
subalgoritmos predefinidos para la gestión de memoria dinámica: ASIGNAR y LIBERAR. Ambos
tienen un único parámetro, de tipo puntero (a cualquier tipo de dato). El subalgoritmo ASIGNAR,
como su propio nombre indica, asigna (reserva) memoria para la variable anónima del puntero
cuyo identificador se le pasa como parámetro. No es necesario indicar el tamaño del dato que
se reserva, ya que la función ASIGNAR se encarga de reservar tanto espacio en memoria como
sea necesario para almacenar el tipo de dato base del puntero. Por su parte, el sugprograma
LIBERAR libera la memoria asignada a la variable anónima del puntero cuyo identificador se
Elementos de Programación
Página 10
ETSI Telecomunicación
Memoria Dinámica
pasa como parámetro. Como ‘efecto lateral’ derivado de la llamada a la función LIBERAR para
un determinado puntero, se le asigna a éste la constante NULO. Por ejemplo,
VARIABLES
R *ptr1, *ptr2 // 1) Declaración de punteros
INICIO
ASIGNAR(ptr1) // 2) Reserva memoria
ASIGNAR(ptr2) // 2) Reserva memoria
*ptr1 = 99.9 // 3) Asignación de valor a la variable anónima
*ptr2 = -33.3 // 3) Asignación de valor a la variable anónima
*ptr1 = *ptr2 // 4) Asigna –333 al dato apuntado por ptr1
ptr1 = ptr2 /* 5) Asignación de punteros.
¡Se pierde la referencia al dato previamente
apuntado por ptr1!!!! */
LIBERAR(ptr2) /* 6) Se libera la memoria a la que apunta ptr2
Se pone ptr2 a NULO */
*ptr1 = -999 // ¡Error! La posición está liberada
1)
2)
ptr1
3)
ptr1
4)
ptr1
¿?
?
ptr2
ptr2
99.9
ptr2
¿?
?
ptr1
-33.3
ptr2
-33.3
-33.3
6)
5)
ptr1
ptr1
-33.3
ptr2
-33.3
ptr2
-33.3
¿?
Figura 2.8. Ilustración de un ejemplo de gestión dinámica de memoria
Elementos de Programación
Página 11
ETSI Telecomunicación
Memoria Dinámica
En la figura 2.8 se ilustra el funcionamiento del ejemplo anterior. Como puede
comprobarse, hay que diferenciar claramente entre la asignación de punteros (operación 5 en
el ejemplo) y la asignación de valores a las variables anónimas correspondientes a esos
punteros (operaciones 3 y 4). Por otro lado, es importante señalar la necesidad de una correcta
gestión de la memoria dinámica que evite dejar posiciones de memoria inaccesibles, como
ocurre como resultado de la operación 5 en el ejemplo anterior, así como acceder a posiciones
de memoria incorrectas (no asignadas al puntero), como ocurre en la operación 6 de dicho
ejemplo.
Es importante señalar aquí que la función ASIGNAR del pseudolenguaje se comporta de
manera ‘ideal’, en el sentido en que se considera que la memoria tiene una capacidad teórica
‘infinita’ y siempre es posible reservar espacio para nuevos datos. En esto la semántica de la
función ASIGNAR difiere de la de las funciones equivalentes en los lenguajes de programación
‘reales’ (incluyendo C/C++) donde sí existen las inevitables limitaciones de recursos de
almacenamiento y no siempre se satisfacen las peticiones de reserva, por lo que dichas
funciones necesitan devolver algún dato que indique si la reserva ha sido efectuada
correctamente.
2.2.4. Punteros a registros
Hasta ahora, en los apartados anteriores, en los que se ha definido el tipo puntero y sus
operaciones básicas, no se ha mostrado la ‘verdadera utilidad’ de los punteros. En los ejemplos
vistos hasta ahora, los punteros manejados han sido meros punteros a datos simples, como
reales, naturales o caracteres. Aunque puede considerarse que se trata de una verdadera
gestión dinámica de memoria, dado que es necesario asignar memoria antes de usar esos
datos y liberarla cuando dejan de usarse, realmente no aporta grandes ventajas a las variables
de tipo estático. La situación cambia cuando el tipo base de las variables puntero es un tipo
complejo, como es el caso de los registros. En ese momento, se pone de manifiesto la
verdadera ‘potencialidad’ de los punteros como elementos para una eficiente gestión de
memoria en tiempo de ejecución, como se verá en el siguiente apartado sobre listas enlazadas.
Como se ha comentado en los apartados anteriores, es posible declarar tipos y variables
de tipo puntero de cualquier tipo base, ya sea éste simple o complejo. Un tipo especial, de gran
utilidad, es el tipo puntero a registro. La declaración de un puntero a un registro sigue la
sintaxis vista en los apartados anteriores. Por ejemplo,
TIPOS
REGISTRO TipoComplejo
R real, imag
FINREGISTRO
TipoComplejo *TipoPtrComplejo
Elementos de Programación
Página 12
ETSI Telecomunicación
Memoria Dinámica
VAR
TipoPtrComplejo ptr
En este último ejemplo la variable ptr es un puntero a un dato de tipo registro,
concretamente de tipo TipoComplejo. En el momento en que tenga lugar una asignación de
memoria a ptr éste apuntará a una posición de memoria donde se almacenan los datos
correspondientes a los dos campos que contiene el registro: real y imag. Esto puede
representarse gráficamente como en la figura 2.9.
ASIGNAR(ptr)
ptr
real
imag
¿?
¿?
Figura 2.9. Asignación de memoria para un puntero a registro
Para acceder a los campos del registro puede combinarse el operador de indirección (*)
con la notación punto, ayudándose de los paréntesis para tener que evitar establecer una
precedencia en estos operadores. Por ejemplo, podría hacerse lo siguiente:
(*ptr).real = 3.33
(*ptr).imag = -9.99
Además de esta notación, el pseudolenguaje introduce una nueva notación mediante el
operador -> (podemos llamarlo ‘flechita’) como simplificación del uso combinado del operador
de indirección y la notación punto. Basta con intercalar este nuevo operador entre el
identificador del puntero y del campo para indicar que se accede a ese campo de la variable
anónima de tipo registro. Por ejemplo, las dos operaciones equivalentes a las anteriores serían:
ptr->real = 3.33
ptr->imag = -9.99
El resultado, de una u otra forma, sería el ilustrado en la figura 2.10.
ptr->real = 3.33
ptr->imag = -9.99
ptr
real
3.33
imag
-9.99
Figura 2.10. Asignación a campos de registros apuntados por punteros
Elementos de Programación
Página 13
ETSI Telecomunicación
Memoria Dinámica
Como se ha comentado más arriba, el tipo base de un puntero puede ser tan complejo
como se quiera, incluyendo registros o arrays que, a su vez, contienen otros registros o arrays
y así sucesivamente. De la misma manera, es posible declarar registros que contienen campos
de tipo puntero e, incluso, arrays de punteros. La utilidad y el uso de cada uno de estos tipos
dependerán de la aplicación. Sirvan simplemente como muestra los siguientes ejemplos, cuyo
funcionamiento se ilustra en la figura 2.11:
TIPOS
REGISTRO TpConstitucion // Registro con dos punteros
R *peso, *altura
FINREGISTRO
C TpNombre[1..100]
REGISTRO TpDatosPersonales
TpNombre nombre
TpConstitucion fisico /* Este campo es un registro
con punteros */
N edad
FINREGISTRO
TpDatosPersonales *TpPtrDatosPersonales // Puntero a registro
TpPtrDatosPersonales TpArrayPtrs[1..200] // Array de punteros
TpNombre *TpPtrArray // Puntero a un array de caracteres
VARIABLES
TpArrayPtrs grupo
TpPtrArray miNombre
...
INICIO
...
ASIGNAR(grupo[1]) /* Reservo memoria para el primer
puntero del array /*
ASIGNAR(grupo[1]->fisico.peso) /* Reservo memoria para el campo
‘peso’ del campo ‘fisico’ del
primer puntero del array */
*(grupo[1]->fisico.peso) = 99.9
ASIGNAR(miNombre) // Reservo memoria para el array de caracteres
*miNombre[1]=’A’ // O bien “(*miNombre)[1] = ‘A’”
...
Elementos de Programación
Página 14
ETSI Telecomunicación
Memoria Dinámica
grupo
...
nombre
‘A’
fisico
‘n’
...
‘e’
edad
‘z’
peso
33
altura
99,9
1.99
Figura 2.11. Estructuras complejas basadas en punteros
2.3. Operaciones sobre Listas Enlazadas
Los punteros y la asignación dinámica de memoria permiten la construcción de estructuras
enlazadas. Una estructura enlazada es una colección de nodos, cada uno de los cuales
contiene uno o más punteros a otros nodos. Cada nodo es un registro en el que uno o más
campos son punteros.
La estructura enlazada más sencilla es la lista enlazada. Una lista enlazada consiste en un
número de nodos, cada uno de los cuales contiene uno o varios datos, además de un puntero;
el puntero permite que los nodos formen una estructura a modo de ‘cadena’ o de ‘lista
enlazada’. En la figura 2.12 puede verse una representación que ilustra este concepto. Como
puede observarse en esta figura, un puntero ‘externo’ apunta al primer nodo de la lista. El
primer nodo es un registro que contiene (además de los campos ‘de datos’) un puntero que
apunta al segundo nodo de la lista, y así sucesivamente. El puntero del último nodo apuntará a
NULO, indicando que es el último nodo de la lista.
Puntero externo
Puntero a siguiente
Campos de datos de
un nodo (registro)
Último nodo de la
lista (siguIente es
NULO)
Figura 2.12. Esquema de lista enlazada con punteros
Elementos de Programación
Página 15
ETSI Telecomunicación
Memoria Dinámica
Para ilustrar la listas enlazadas se mostrará a continuación cómo definir, crear y manipular
(insertar elementos, buscar, eliminar, etc.) una lista enlazada cuyos nodos contienen un único
campo de datos (además de un puntero al siguiente nodo) de tipo carácter (C). Como se ha
dicho anteriormente, los elementos de las listas (en este caso caracteres) se guardan como
campos de registros que, además, contienen un puntero que sirve de enlace con el siguiente
nodo de la lista que contiene el siguiente elemento.
Para este ejemplo se va a declarar el tipo TpLista, que se definirá como un tipo puntero a
un tipo registro TpNodo que contiene un campo de tipo carácter y un puntero de tipo TpLista.
Nótese que la declaración de TpLista es ‘recursiva’, en el sentido de que necesita TpNodo
para efectuarse, a la vez que TpNodo hace uso nuevamente de TpLista. Esta ‘licencia’ se le
concede al pseudolenguaje de la asignatura para aumentar su expresividad y permitir este tipo
de declaraciones en las que se declara un tipo en base a otro tipo que, a su vez, vuelve a
recurrir al primero en su definición.
TIPOS
TpNodo *TpLista
REGISTRO TpNodo
C caracter // Elemento del nodo
TpLista sig // Puntero al siguiente nodo
FINREGISTRO
Otra manera de hacer esto, completamente equivalente a la anterior, consiste en declarar
primero el registro, declarando el puntero sig como un puntero al mismo TpNodo para,
posteriormente, declarar TpLista. De esta manera,
TIPOS
REGISTRO TpNodo
C caracter // Elemento del nodo
TpNodo *sig // Puntero al siguiente nodo
FINREGISTRO
TpNodo *TpLista
lista
a
b
c
Figura 2.13. Lista enlazada de caracteres
En cualquier caso, se puede dibujar cada nodo de la lista enlazada como una ‘caja’ con dos
campos: un carácter y un puntero. De esta forma, en la figura 2.13 se muestra una lista
enlazada con tres caracteres donde lista es una variable de tipo TpLista (puntero ‘externo’)
Elementos de Programación
Página 16
ETSI Telecomunicación
Memoria Dinámica
que apunta al primer nodo de la lista enlazada. Para ello sería necesaria, en primer lugar, la
siguiente declaración de datos:
VARABLES
TpLista lista
Con esta estructura declarada, para almacenar una cadena de caracteres de cualquier
tamaño bastará con ir leyendo (por ejemplo, por teclado) los caracteres uno a uno e ir creando
para cada uno un nuevo nodo (variable anónima de tipo registro) donde almacenarlo, haciendo
siempre que cada nodo enlace con el siguiente y que el último nodo de la lista apunte a NULO.
Si la entrada se realiza por teclado, puede establecerse un convenio para indicar dónde acaba
la introducción de caracteres. Por ejemplo, el retorno de carro (carácter 13 en la tabla ASCII)
puede servir de indicador de fin de la entrada de datos.
A partir de lo expuesto, pueden identificarse tres operaciones básicas sobre listas
enlazadas:
•
Creación de una lista enlazada vacía.
•
Inserción de un nuevo nodo en la lista enlazada.
•
Eliminación de un nodo en la lista enlazada.
Creación de una lista enlazada vacía
El subalgoritmo para la creación de una lista enlazada vacía (sin ningún elemento inicialmente)
es relativamente simple: basta con inicializar el puntero ‘externo’ a NULO. Este algoritmo
recibirá como parámetro de ES un elemento de tipo TpLista.
ALGORITMO CrearLista(ES TpLista lista)
INICIO
lista = NULO
FIN CrearLista
Inserción de un nuevo nodo en la lista enlazada
En la operación de inserción de un nodo en una lista enlazada pueden distinguirse dos casos
claramente diferenciados:
•
Inserción de un nodo al principio de una lista.
•
Inserción de un nodo después de un determinado nodo existente (por ejemplo, de
manera que la lista se mantenga ordenada en orden ascendente).
Elementos de Programación
Página 17
ETSI Telecomunicación
Memoria Dinámica
1) Inserción de un nuevo nodo al principio de una lista enlazada
Para insertar un nuevo nodo al comienzo de una lista enlazada deben seguirse los siguientes
pasos:
1. Crear un nodo para una variable anónima auxiliar, previamente declarada de tipo TpLista.
ASIGNAR(ptr) // ptr antes declarado como ‘TpLista ptr’
2. Asignar un valor (carácter) al campo de datos de la nueva variable anónima.
ptr->caracter = ‘d’
3. Hacer que el campo siguiente del nuevo nodo apunte donde actualmente apunte el puntero
‘externo’ de la lista, es decir, lista.
ptr->sig = lista
4. Por último, debe actualizarse el puntero ‘externo’ de la lista, es decir, lista, para que
apunte al primer nodo de la lista, que será el que se acaba de introducir.
lista = ptr
Todo estos pasos se dan en el siguiente subalgoritmo para insertar un nuevo elemento al
principio de una lista de caracteres (cuyo funcionamiento se ilustra gráficamente en la figura
2.14), que recibirá como parámetros la lista (puntero de tipo TpLista) y el carácter a insertar
en el nuevo nodo:
ALGORITMO InsertarAlPrincipio(ES TpLista lista; E C car)
VARIABLES
TpLista ptr // Puntero auxiliar
INICIO
ASIGNAR(ptr) // Nuevo nodo
ptr->caracter = car
ptr->sig = lista
lista = ptr
FIN InsertarAlPrincipio
Elementos de Programación
Página 18
ETSI Telecomunicación
Memoria Dinámica
1) y 2)
ASIGNAR(ptr)
ptr->caracter = ‘d’
ptr
d
3) ptr->sig = lista
4) lista=ptr
ptr
a
4)
3)
lista
a
b
c
Figura 2.14. Inserción de un nodo al principio de una lista enlazada de caracteres
2) Inserción de un nuevo nodo en una posición determinada de la lista
Cuando se desea insertar un nuevo elemento en una lista enlazada ordenada, de manera que
esta permanezca ordenada después de la inserción, los pasos a seguir serían los siguientes
(para el caso de una lista de caracteres ordenados alfabéticamente en orden ascendente):
1. Si la lista está vacía o el primer elemento de la lista es mayor (alfabéticamente) que el
elemento a insertar, se procede como en el caso anterior en el que se insertaba el elemento al
principio de la lista. En otro caso, se sigue con los puntos siguientes.
2. Crear el nuevo nodo, reservando espacio para el mismo y almacenando en su campo de
datos (caracter) el nuevo elemento que se desea insertar. Es aconsejable utilizar, para ello,
una variable auxiliar de tipo TpLista, por ejemplo, la variable nuevoNodo. Opcionalmente,
puede ponerse el puntero sig del nuevo nodo apuntando inicialmente a NULO.
ASIGNAR(nuevoNodo)
nuevoNodo->caracter = ‘d’
nuevoNodo->sig = NULO
3. Se utiliza una nueva variable auxiliar ptr, de tipo TpLista, para recorrer cada uno de los
nodos de la lista hasta encontrar en lugar exacto donde debe insertarse el nuevo elemento;
para ello, ptr estará siempre apuntando al nodo anterior al nodo cuyo elemento se está
comparando con el elemento a insertar, de manera que una vez que se localice el lugar de
inserción sea posible enlazar correctamente el nuevo nodo en la lista, manteniéndola
ordenada. Esto se haría de la siguiente manera, para el ejemplo que se viene mostrando:
Elementos de Programación
Página 19
ETSI Telecomunicación
Memoria Dinámica
ptr = lista
MIENTRAS (ptr->sig != NULO) Y
(nuevoNodo->carácter > ptr->sig->caracter) HACER
ptr = ptr->sig
FINMIENTRAS
nuevoNodo->sig = ptr->sig
ptr->sig = nuevoNodo
El algoritmo quedaría de la siguiente manera (gráficamente, en la figura 2.15):
ALGORITMO InsertarOrdenada(ES TpLista lista; E C car)
VARIABLES
TpLista nuevoNodo, ptr
INICIO
SI lista == NULO O lista->carácter >= car ENTONCES
InsertarAlPrincipio(lista, car)
SINO
ASIGNAR(nuevoNodo)
nuevoNodo->caracter = car
nuevoNodo->sig = NULO
ptr = lista
MIENTRAS ptr->sig != NULO Y
(car > ptr->sig->caracter HACER
ptr = ptr->sig
FINMIENTRAS
nuevoNodo->sig = ptr->sig
ptr->sig = nuevoNodo
FINSI
FIN InsertarOrdenada
Elementos de Programación
Página 20
ETSI Telecomunicación
Memoria Dinámica
2)
ASIGNAR(nNodo)
nNodo->caracter = ‘c’
nNodo->sig = NULO
nNodo
c
3) /* Localización
del lugar de
inserción (bucle)*/
ptr
lista
a
b
3) /* Enlace de
punteros para
mantener la
ordenación */
d
nNodo
c
lista
a
b
d
ptr
Figura 2.15. Inserción de un nodo en una lista ordenada
Eliminación de un nodo de una lista enlazada
Como en el caso de la inserción de nuevos nodos, en la operación de eliminar un nodo de una
lista enlazada pueden también diferenciarse dos situaciones:
•
Borrar el primer nodo de la lista enlazada.
•
Borrar un determinado nodo de la lista enlazada.
1) Borrar el primer nodo de una lista enlazada
Los pasos a seguir son los siguientes:
1. Declaración de un puntero auxiliar, que se inicializará de manera que apunte al primer nodo
de la lista, que es el que se desea borrar.
ptr = lista
Elementos de Programación
Página 21
ETSI Telecomunicación
Memoria Dinámica
2. Se actualiza el puntero ‘externo’ de la lista (lista) para que apunte al segundo elemento de
la misma, si existe, o bien a NULO. Para ello, basta con hacer:
lista = lista->sig
3. Por último, es importante liberar la memoria correspondiente al nodo que se desea eliminar y
al que está apuntando actualmente el puntero auxiliar ptr.
LIBERAR(ptr)
El algoritmo (ilustrado en la figura 2.16) quedaría de la siguiente manera:
ALGORITMO EliminarPrimero(ES TpLista lista)
VARIABLES
TpLista ptr
INICIO
SI lista != NULO ENTONCES // Si no, no hay que eliminar nada
ptr = lista
lista = lista->sig
LIBERAR(ptr)
FINSI
FIN EliminarPrimero
1)
ptr
lista
a
2) y 3)
b
c
b
c
ptr
lista
a
Figura 2.16. Borrar el primer nodo de una lista enlazada
2) Borrar un determinado nodo de la lista enlazada
Para borrar un nodo concreto de la lista enlazada ordenada (diferente del primero) son
necesarios dos punteros auxiliares: uno apuntando al nodo a borrar, que tendrá el nombre de
Elementos de Programación
Página 22
ETSI Telecomunicación
Memoria Dinámica
ptr en el algoritmo que sigue, y otro puntero que apunte al nodo anterior al nodo que debe
eliminarse, que se denominará ant. De este modo, la operación de ‘enlace’ de los nodos para
‘saltar’ el nodo eliminado será bastante simple, como se muestra en algoritmo
BorrarOrdenada. La localización del nodo a borrar debe tener en cuenta el hecho de que la
lista está ordenada y de que puede que el elemento no exista. Si la lista no estuviese
ordenada, lo único que sería diferente en este algoritmo sería la forma de localizar el nodo a
borrar.
ALGORITMO BorrarOrdenada(ES TpLista lista; E C car)
VARIABLES
TpLista ptr, ant=NULO
INICIO
SI lista != NULO ENTONCES // Si no, no hay que hacer nada
ptr = lista
MIENTRAS ptr != NULO Y ptr->caracter != car HACER
ant = ptr
ptr = ptr->sig
FINMIENTRAS
SI ptr != NULO ENTONCES // Encontrado
SI ant == NULO ENTONCES // Es el primer elemento
lista = lista->sig
SINO
ant->sig = ptr->sig
FINSI
LIBERAR(ptr)
FINSI
FINSI
FIN BorrarOrdenada
En la figura 2.17 se muestra la operación de eliminación de un nodo (que contiene la letra
‘b’) en una lista enlazada ordenada.
ant
ptr
a
b
lista
c
Figura 2.17. Eliminación de un nodo en una lista enlazada ordenada
Elementos de Programación
Página 23
ETSI Telecomunicación
Memoria Dinámica
Otras operaciones con listas enlazadas
Puede completarse el conjunto de operaciones básicas con listas enlazadas incorporando dos
nuevas operaciones, de gran utilidad: visualizar una lista escribiéndola, por ejemplo, en la
salida estándar (pantalla) y eliminar todos los nodos de una lista. Esta última operación es
necesaria en situaciones donde es preciso ‘eliminar’ una lista completa y liberar la memoria
ocupada por todos sus nodos. A continuación se muestran ambos subalgoritmos.
ALGORITMO EscribirLista(E TpLista lista)
VARIABLES
TpLista ptr = lista
INICIO
MIENTRAS ptr != NULO HACER
Escribir(ptr->caracter)
ptr = ptr->sig
FINMIENTRAS
FIN EscribirLista
ALGORITMO BorrarLista(ES TpLista lista)
VARIABLES
TpLista ptr
INICIO
MIENTRAS lista != NULO HACER
ptr = lista
lista = lista->sig
LIBERAR(ptr)
FINMIENTRAS
FIN BorrarLista
2.3.1. Otras clases de listas enlazadas
Otras clases de listas enlazadas son las listas doblemente enlazadas. A diferencia de las listas
enlazadas vistas en los apartados anteriores, las listas doblemente enlazadas contienen dos
punteros en cada nodo, además de los campos de datos propiamente dichos. Uno de estos
punteros, de manera similar a las listas enlazadas ‘simples’, apunta al siguiente nodo de la
lista, mientras que el otro puntero apunta al nodo anterior, tal y como se ilustra en la figura
2.18. Disponer de dos enlaces en lugar de uno tiene varias ventajas:
§
La lista puede recorrerse en cualquier dirección. Esto simplifica la gestión de la lista,
facilitando las inserciones y las eliminaciones.
Elementos de Programación
Página 24
ETSI Telecomunicación
§
Memoria Dinámica
Mayor tolerancia a fallos. Se puede recorrer la lista tanto con los enlaces ‘hacia delante’
como con los enlaces ‘hacia atrás’, con lo que si algún enlace queda invalidado por
algún error, se puede reconstruir la lista utilizando el otro enlace.
El inconveniente principal de estas listas es que a la hora de realizar operaciones de
inserción o eliminación de nodos es mayor el número de punteros que hay que ‘mover’ para
mantener la lista correctamente enlazada. Eso requiere que la implementación de las
operaciones deba ser más ‘cuidadosa’ que en el caso de las listas simples.
Existe un caso especial de lista doblemente enlazada donde el puntero que apuntan al
nodo anterior del primer nodo de la lista, en lugar de estar apuntando a NULO, apunta al último
elemento de la lista, mientras que el puntero que apunta al nodo siguiente del último nodo de la
lista, en lugar de apuntar a NULO, apunta al primer nodo de la lista. Este tipo de lista se
denomina lista doblemente enlazada circular y permite, entre otras cosas, hacer recorridos
completos de la lista sin necesidad de empezar en el primer nodo y sin tener que cambiar el
‘sentido’ del recorrido. También es posible implementar listas simples circulares, donde el
campo sig del último nodo de la lista apunta al primer nodo de la lista.
lista
datos
datos
datos
Figura 2.18. Lista doblemente enlazada
La forma de construir una lista doblemente enlazada es similar a la de la lista enlazada
simple, con la principal diferencia de que hay que mantener dos enlaces en lugar de uno. Por
tanto, el registro que constituye cada nodo debe contener, además de los campos de datos,
dos campos de tipo puntero a un nodo. Siguiendo con listas de caracteres, puede hacerse:
TIPOS
TpNodo *TpListaDoble
REGISTRO TpNodo
C caracter
TipoListaDoble *ant, *sig
FINREGISTRO
Las operaciones básicas realizables con listas doblemente enlazadas coinciden con el caso
de las listas enlazadas simples, y son:
§
Creación de una lista doblemente enlazada.
§
Inserción de un nodo en una lista doblemente enlazada.
§
Eliminación de un nodo en una lista doblemente enlazada.
Elementos de Programación
Página 25
ETSI Telecomunicación
Memoria Dinámica
Creación de una lista doblemente enlazada
Una lista doblemente enlazada se crea de la misma forma que una lista enlazada ‘simple’, esto
es, inicializando el puntero ‘externo’ de la lista a NULO para indicar que la lista está vacía.
ALGORITMO CrearListaDoble (ES TpListaDoble listaDoble)
INICIO
listaDoble = NULO
FIN CrearListaDoble
Inserción de un nodo en una lista doblemente enlazada
En la operación de insertar un nodo en una lista doblemente enlazada pueden distinguirse dos
casos claramente diferenciados, como en el caso de las listas simples:
§
Inserción de un nodo al principio de la lista.
§
Inserción del nodo después de un determinado nodo existente (por ejemplo, para
mantener la lista ordenada alfabéticamente en orden ascendente).
1) Inserción de un nodo al principio de una lista doblemente enlazada
El algoritmo para insertar un nodo al comienzo de una lista doblemente enlazada que ha sido
previamente creada (precondición necesaria) se muestra a continuación:
ALGORITMO InsertaAlPrincipioDoble(ES TpListaDoble listaDoble; E C car)
VARIABLES
TpListaDoble ptr
INICIO
ASIGNAR(ptr) // Reserva de memoria
ptr->caracter = car // Inicialización de campos del nuevo nodo
ptr->ant = NULO
ptr->sig = NULO
SI (listaDoble != NULO) ENTONCES // Lista no vacía
ptr->sig = listaDoble
listaDoble->ant = ptr
FINSI
listaDoble = ptr // Nuevo primer nodo de la lista
FIN InsertaAlPrincipioDoble
2) Inserción de un nodo en una posición determinada de una lista doblemente enlazada
Para insertar un nodo en una lista doblemente enlazada ordenada, de manera que se
mantenga ordenada después de la inserción, es necesario, en primer lugar, localizar la posición
de la lista donde debe insertarse el nodo. Un algoritmo para insertar un nodo en una posición
determinada de una lista enlazada doble es el siguiente:
Elementos de Programación
Página 26
ETSI Telecomunicación
Memoria Dinámica
ALGORITMO InsertarOrdenadaDoble (ES TpListaDoble listaDoble; E C car)
VARIABLES
TpListaDoble ptr, ant, nuevo
INICIO
SI listaDoble == NULO O listaDoble->caracter >= car) ENTONCES
InsertaAlPrincipoDoble(listaDoble, car)
SINO
ASIGNAR(nuevo)
ant = listaDoble
ptr = listaDoble->sig // Apunta al segundo nodo o NULO
MIENTRAS ptr != NULO Y car > ptr->caracter HACER
ant = ptr
ptr = ptr->sig
FINMIENTRAS
SI ptr == NULO ENTONCES // Se inserta al final
nuevo->ant = ant
ant->sig = nuevo
SINO // Se inserta en medio de la lista
nuevo->sig = ptr
nuevo->ant = ant
ant->sig = nuevo
ptr->ant = nuevo
FINSI
FINSI
FIN InsertarOrdenadaDoble
En la figura 2.19 se muestra el funcionamiento de este algoritmo. Nótese el ‘movimiento’ de
punteros necesario para mantener la lista enlazada.
listaDoble
ant
ptr
a
c
k
v
f
Figura 2.19. Inserción en una lista doblemente enlazada
Elementos de Programación
Página 27
ETSI Telecomunicación
Memoria Dinámica
Eliminación de un nodo de una lista doblemente enlazada
En la operación de eliminar un nodo de una lista doblemente enlazada pueden distinguirse dos
casos claramente diferenciados, como en el caso de las listas simples:
§
Borrar el primer nodo de la lista.
§
Borrar un determinado nodo existente (por ejemplo, de forma que se mantenga la lista
ordenada alfabéticamente en orden ascendente después de la operación de borrar).
1) Borrar el primer nodo de la lista doblemente enlazada
ALGORITMO EliminarPrimeroDoble(ES TpListaDoble listaDoble)
VARIABLES
TpListaDoble ptr
INICIO
SI listaDoble != NULO ENTONCES // Si no, no hacemos nada
ptr = listaDoble
listaDoble = listaDoble->sig
LIBERAR(ptr)
FINSI
FIN EliminarPrimeroDoble
2) Borrar un nodo de una posición determinada de la lista
Para borrar un nodo de una posición determinada de la lista se necesitan dos punteros
auxiliares: uno apuntando al nodo a borrar, que se denominará ptr en el algoritmo que se
muestra a continuación; y otro que apunte al nodo anterior al nodo que se va a eliminar, que se
llamará ant. Esto permitirá dejar la lista correctamente enlazada después de la liberación de la
memoria correspondiente al nodo borrado.
ALGORITMO BorrarOrdenadaDoble(ES TpListaDoble listaDoble; E C car)
VARIABLES
TpListaDoble ant, ptr
INICIO
ant = NULO
ptr = listaDoble
MIENTRAS ptr != NULO Y ptr->caracter != car HACER
ant = ptr
ptr = ptr->sig
FINMIENTRAS
SI ptr != NULO ENTONCES // Se ha encontrado el elemento
SI ant == NULO ENTONCES
// El elemento a borrar es el primero
Elementos de Programación
Página 28
ETSI Telecomunicación
Memoria Dinámica
listaDoble = listaDoble->sig
SI listaDoble != NULO ENTONCES
// Hay más nodos en la lista
listaDoble->ant = NULO
FINSI
SINO SI ptr->sig == NULO ENTONCES // Borrar el último
ant->sig = NULO
SINO // El elemento a borrar está en medio de la lista
ant->sig = ptr->sig
ptr->sig->ant = ant
FINSI
LIBERAR(ptr)
FINSI
FIN BorrarOrdenadaDoble
Elementos de Programación
Página 29
ETSI Telecomunicación
Memoria Dinámica
2.4. Ejemplo de Gestión de Memoria Dinámica
en C/C++
En C/C++ la sintaxis de declaración de punteros es similar a la vista para el pseudolenguaje en
los apartados anteriores: se usa el asterisco (*) a continuación del tipo base para indicar que
se trata de un puntero a ese tipo base. Por otro lado, la operación de asignación de memoria se
lleva a cabo mediante la función new. Para ello se asigna al puntero el resultado de ‘invocar’ la
función new para el tipo base del mismo, como se muestra en el ejemplo a continuación. La
función delete equivale al subalgoritmo ASIGNAR del pseudolenguaje para la liberación de
memoria. El siguiente ejemplo se presenta como una simple muestra de la sintaxis de C/C++
en el manejo de memoria dinámica mediante el uso de punteros2:
#include <iostream>
#include <stdlib.h>
struct TpNodo{
char car;
TpNodo *sig;
};
typedef TpNodo *TpPtrNodo;
void main(){
TpPtrNodo ptr;
ptr = new TpNodo; // Asignación de memoria
ptr->car = 'a';
ptr->sig = NULL; // NULL equivale a NULO en el pseudolenguaje
cout << ptr->car << endl;
delete ptr; // Liberación de memoria apuntada por ptr
system("pause");
}
2
En la asignatura de Laboratorio de Programación se presentarán estos conceptos de manera más extensa y
detallada.
Elementos de Programación
Página 30
ETSI Telecomunicación
Memoria Dinámica
Ejercicios Propuestos
1) Dada la siguiente declaración de tipos adjunta:
TIPOS
Nodo *Lista
REGISTRO Nodo
Z elem
Lista sig
FINREGISTRO
a) Diseña un algoritmo que imprima cada uno de los elementos
de una lista.
b) Diseña un algoritmo que devuelva una copia de una lista.
c) Diseña un algoritmo que devuelva la longitud de una lista.
d) Diseña un algoritmo que elimine el último elemento de una
lista.
e) Diseña un algoritmo que ordene los elementos de una lista.
2) Dada la declaración de tipos anterior y las variables l1 y l2 de tipo Lista, y siendo l1 la
siguiente lista enlazada:
l1
3
5
7
a) ¿Qué diferencia existe entre las dos
ALGORITMO Ejemplo(E Lista p)
instrucciones l2=l1 y Copiar(l1, l2), la
INICIO
cual duplica una lista?
SI p !=NULO ENTONCES
b) ¿Qué valor contiene l2->sig tras realizar la
p->elem = 3
secuencia de instrucciones?:
FINSI
l2=l1; LIBERAR(l1)
FIN Ejemplo
c) Dado el algoritmo Ejemplo adjunto, donde p
se pasa por valor, y el estado inicial de la lista
l1, ¿qué ocurrirá a la lista apuntada por l1 tras la siguiente llamada: Ejemplo(l1)?
3) Dada la declaración de tipos adjunta:
a) Diseña un algoritmo que inserte un elemento al inicio de una
lista.
b) Diseña un algoritmo que inserte un elemento al final de una
lista.
c) Diseña un algoritmo que elimine el primer elemento de una
lista.
d) Diseña un algoritmo que elimine el último elemento de una
lista.
e) Diseña un algoritmo que elimine los datos correspondientes a
una persona con un nombre determinado de una lista.
f) Diseña un algoritmo que devuelva una copia de una lista.
g) Diseña un algoritmo que devuelva la longitud de una lista.
h) Diseña un algoritmo que ordene una lista por el nombre de las personas.
REGISTRO Persona
C nombre[0..99]
N teléfono
FINREGISTRO
Nodo *Lista
REGISTRO Nodo
Persona elem
Lista sig
FINREGISTRO
Elementos de Programación
Página 31
ETSI Telecomunicación
Memoria Dinámica
4) Dadas las definiciones de tipos adjuntas, diseñar los siguientes algoritmos:
a) Borrar todos los nodos de una lista enlazada y liberar
Nodo *TipoPuntero
toda la memoria.
REGISTRO Nodo
b) Duplicar una lista enlazada.
N valor
c) Borrar el nodo que contiene el máximo valor de una
TipoPuntero sig
lista enlazada.
FINREGISTRO
d) Intercambiar el valor n-ésimo con el m-ésimo de la
lista.
e) Concatenar dos listas enlazadas.
f) Borrar el n-ésimo elemento de la lista.
g) Diseñar el algoritmo con la siguiente cabecera:
ALGORITMO TipoPuntero Busca(E TipoPuntero lista; E N elem)
que devuelva el puntero al nodo que contiene el natural elem, si existe, y NULO en
caso contrario.
h) Dada la siguiente cabecera:
ALGORITMO TipoPuntero InsBusca(ES TipoPuntero lista; E N elem)
añada elem a lista, si no está en ella, y siempre devuelve un puntero al nodo
que contiene elem.
5) Sea el tipo TipoLista adjunto. Resolver los siguientes apartados en base a este
tipo.
Nodo *TipoLista
REGISTRO Nodo
N valor
TipoLista sig
FINREGISTRO
a) Diseña un algoritmo Purgar(...) que elimine todos los
elementos duplicados una lista.
b) Diseña un algoritmo BorrarUltimo(...) que elimine de
una lista el último nodo que contiene la información k.
c) Dadas dos listas enlazadas ordenadas, l1 y l2 de tipo
TipoLista, escribe un algoritmo que mezcle las dos listas
(pasadas como parámetros) en otra, de forma que esta
última esté también ordenada. l1 y/o l2 pueden estar
vacías. Resolverlo de dos formas:
• Sin modificar las listas l1 y l2, creando una
lista nueva.
• Modificando las listas l1 y l2, sin reservar
memoria adicional.
6) Hay muchas aplicaciones en las que se debe almacenar en la memoria un vector de
grandes dimensiones. Si la mayoría de los elementos del vector son ceros, éste puede
representarse más eficientemente utilizando una lista enlazada con punteros, en la que
cada nodo es un registro con tres campos: el dato en esa posición si es distinto de cero, el
índice de esa posición y un puntero al siguiente nodo. Por ejemplo, para un vector de
longitud 7 la lista
25
1
14
5
representa el vector (25, 0, 0, 0, -14, 0, 0). Dado un tipo TVector de longitud
constante Dim diseña algoritmos para:
a) Calcular su Producto_Escalar, pasándole como entrada v1 y v2 de tipo TVector.
Devolverá un vector nuevo.
b) Insertar un valor teniendo en cuenta que si ya existe, se debe eliminar el valor
anterior. Si la posición está fuera de rango, no hará nada. Y si es cero el valor a
insertar no deberá quedar guardado en la lista ningún nodo de índice cero.
Elementos de Programación
Página 32
ETSI Telecomunicación
Memoria Dinámica
c) Obtener el valor en una determinada posición del vector.
d) Mejorar los apartados anteriores redefiniendo el tipo TVector como un registro de dos
campos, uno de ellos contendrá el vector en el formato anterior y el otro indicará la
longitud, la cual ya no será necesariamente constante.
7) Una forma de almacenar un número natural de valor mayor que el permitido en una
computadora es introducir cada dígito, de tipo natural, en un nodo de una lista enlazada.
Por ejemplo, la siguiente lista representa al número 357:
numero
3
5
7
a) Escribe un algoritmo que tome como parámetro un puntero a una lista enlazada que
represente un número de la forma indicada y devuelva el número correspondiente en
una variable de tipo natural.
b) Diseña también un algoritmo que lea por teclado una sucesión de dígitos, de tipo
carácter, y los introduzca como dígitos de tipo natural en una lista enlazada.
c) Diseña dos algoritmos que realicen, respectivamente, la suma y producto de números
representados de esta forma.
8) Un polinomio en x, de tipo entero, de grado arbitrario se puede representar mediante una
lista enlazada, donde cada nodo contiene 1) el coeficiente, 2) el exponente de un término
del polinomio, y 3) un puntero al siguiente nodo. Por ejemplo, el polinomio P(x)≡25x –14x5
se representaría como indica la figura adjunta:
p
25
1
-14
5
a) Define el tipo TPolinomio que represente un polinomio mediante una lista enlazada
con punteros.
b) Escribe una función que evalúe un polinomio en un punto x:
ALGORITMO Z Evaluar(E TPolinomio p; E Z valor)
c) Escribe una función que obtenga el coeficiente del término de un determinado grado:
ALGORITMO TPolinomio Coeficiente(E TPolinomio p1; E N grado)
d) Escribe una función que sume 2 polinomios p1 y p2:
ALGORITMO TPolinomio Sumar(E TPolinomio p1, p2)
e) Escribe una función que realice la derivada de un polinomio P, con la siguiente
cabecera:
ALGORITMO TPolinomio Derivada(E TipoPolinomio p)
9) Supón que tienes diseñado el tipo conjunto (colección no ordenada de elementos distintos)
mediante una lista enlazada dinámica con los tipos adjuntos:
a) Queremos diseñar las operaciones intersección
Elemento *Conjunto
y unión de dos conjuntos: ¿Cómo serían las
REGISTRO Elemento
cabeceras de los dos algoritmos que realizaran
Z elem
estas tareas?. Al diseñar estos algoritmos,
Conjunto sig
¿modificas alguno de los dos conjuntos con los
FINREGISTRO
que operas? ¿Se ve eso reflejado en las
cabeceras?
b) Diseña los algoritmos de acuerdo con las cabeceras del apartado a).
Elementos de Programación
Página 33
ETSI Telecomunicación
Memoria Dinámica
Bibliografía
Aho, A. V., Hopcroft, J. E., & Ullman, J. D. (1988). Estructuras de Datos y Algoritmos. Addison
Wesley Iberoamericana.
Cerrada, J. A., & Collado, M. (1995). Programación I. UNED.
Dale, N., & Weems, C. (1989). Pascal (2 ed.). McGraw Hill.
Helman, P., Veroff, R., & Carrano, F. (1991). Intermediate Problem Solving and Data
Structures. Walls & Mirrors (2 ed.). The Benjamin/Cummings Publishing.
Horowitz, E., & Sahni, S. (1999). Fundamentals of Data Structures in Pascal (4 ed.). W. H.
Freeman & Co.
Joyanes, L. (1996). Fundamentos de Programación. Algoritmos y Estructuras de Datos. (2 ed.).
McGraw Hill.
Langsam, Y., Augenstein, M. J., & Tanenbaum, A. M. (1995). Data Structures using C and C++
(2 ed.). Prentice Hall.
Weiss, M. A. (1995). Estructuras de Datos y Algoritmos. Addison Wesley Iberoamericana.
Elementos de Programación
Página 34
Descargar