34
ETSI Telecomunicación Memoria Dinámica Elementos de Programación Página 1 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

Tema 2. Memoria Dinámica - lcc.uma.eslcc.uma.es/~fvn/EleProg/punteros_apuntes_y_ejercicios.pdf · § Ejemplo de Gestión de Memoria Dinámica en C/C++ § Ejercicios Propuestos

Embed Size (px)

Citation preview

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 1

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 2

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 3

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

Pila

Límite de datos estáticos

Límite de la pila

Puntero de la pila (stack pointer)

Zona dinámica fragmentada (heap)

Figura 2.1. Esquema de asignación de memoria

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 4

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.

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 5

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.

... 78AC(16 ... AACC(16 6743(16 6677(16 89FF(16 DC34(16 ...

... 7881(16 ...

78AA(16 78AB(16 78AC(16 78AD(16 78AE(16 ...

Puntero

Variable referenciada

Dirección Contenido

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).

... ... ... ‘z’ ... ... ...

Dirección 7C16(16 7C17(16 7C18(16

VARIABLES C car = ‘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.

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 6

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

33

ptr

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.

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 7

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 (*).

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 8

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.

... ... -333 999 ... ... 7C17

Dirección 7C16 7C17 7C18

valor ptrValor dato

ptrValor = &valor

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

... ... 999 999 ... ... 7C17

Dirección 7C16 7C17 7C18

valor ptrValor dato

dato = *ptrValor

Figura 2.6. Operador de indirección

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 9

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

ptrReal = NULO

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 10

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 11

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

ptr1

ptr2

ptr1

ptr2

?

?

¿?

¿?

ptr1

ptr2

99.9

-33.3

1)

ptr1

ptr2

-33.3

-33.3

ptr1

ptr2

-33.3

-33.3

2) 3) 4)

5)

ptr1

ptr2

-33.3

¿?

6)

Figura 2.8. Ilustración de un ejemplo de gestión dinámica de memoria

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 12

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 13

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.

¿?

ptr

¿?

ASIGNAR(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.

3.33

ptr

-9.99

ptr->real = 3.33 ptr->imag = -9.99

real imag

Figura 2.10. Asignación a campos de registros apuntados por punteros

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 14

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’”

...

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 15

...

grupo

33 ‘A’ ‘n’ ... ‘e’ ‘z’

nombre fisico edad

peso 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

Campos de datos de un nodo (registro)

Puntero a siguiente

Último nodo de la lista (siguIente es NULO)

Figura 2.12. Esquema de lista enlazada con punteros

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 16

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’)

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 17

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).

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 18

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 19

lista

a b c

1) y 2) ASIGNAR(ptr) ptr->caracter = ‘d’

ptr

d

3) ptr->sig = lista 4) lista=ptr

ptr

a

3) 4)

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:

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 20

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 21

lista

a b d

2) ASIGNAR(nNodo) nNodo->caracter = ‘c’ nNodo->sig = NULO

nNodo

c

3) /* Localización del lugar de inserción (bucle)*/

ptr

lista

a b d

nNodo

c

ptr

3) /* Enlace de punteros para mantener la ordenación */

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 22

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

lista

a b c

ptr

lista

a b c

ptr

1)

2) y 3)

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 23

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.

lista

a b c

ptr ant

Figura 2.17. Eliminación de un nodo en una lista enlazada ordenada

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 24

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.

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 25

§ 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.

datos datos datos

lista

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.

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 26

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:

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 27

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.

c k a v

f

ant

ptr

listaDoble

Figura 2.19. Inserción en una lista doblemente enlazada

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 28

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 29

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

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 30

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.

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 31

Ejercicios Propuestos

1) Dada la siguiente declaración de tipos adjunta:

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

instrucciones l2=l1 y Copiar(l1, l2), la cual duplica una lista?

b) ¿Qué valor contiene l2->sig tras realizar la secuencia de instrucciones?: l2=l1; LIBERAR(l1)

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.

TIPOS Nodo *Lista REGISTRO Nodo

Z elem Lista sig FINREGISTRO

ALGORITMO Ejemplo(E Lista p) INICIO SI p !=NULO ENTONCES p->elem = 3 FINSI FIN Ejemplo

REGISTRO Persona C nombre[0..99] N teléfono FINREGISTRO Nodo *Lista REGISTRO Nodo Persona elem Lista sig FINREGISTRO

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 32

4) Dadas las definiciones de tipos adjuntas, diseñar los siguientes algoritmos: a) Borrar todos los nodos de una lista enlazada y liberar

toda la memoria. b) Duplicar una lista enlazada. c) Borrar el nodo que contiene el máximo valor de una

lista enlazada. 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.

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

1 25 5 14

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.

Nodo *TipoPuntero REGISTRO Nodo N valor TipoPuntero sig FINREGISTRO

Nodo *TipoLista REGISTRO Nodo N valor TipoLista sig FINREGISTRO

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 33

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:

1 25 5 -14

p

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

y unión de dos conjuntos: ¿Cómo serían las cabeceras de los dos algoritmos que realizaran estas tareas?. Al diseñar estos algoritmos, ¿modificas alguno de los dos conjuntos con los que operas? ¿Se ve eso reflejado en las cabeceras?

b) Diseña los algoritmos de acuerdo con las cabeceras del apartado a).

Elemento *Conjunto REGISTRO Elemento Z elem Conjunto sig FINREGISTRO

ETSI Telecomunicación Memoria Dinámica

Elementos de Programación Página 34

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.