Enjoying your free trial? Only 9 days left! Upgrade Now
Brand-New
Dashboard lnterface
ln the Making
We are proud to announce that we are developing a fresh new dashboard interface to improve user experience.
We invite you to preview our new dashboard and have a try. Some features will become unavailable, but they will be added in the future.
Don't hesitate to try it out as it's easy to switch back to the interface you're used to.
No, try later
Go to new dashboard
Like
Share
Download
Create a Flipbook Now
Read more
4ta Edición
Luis Joyanes Aguilar
435-796
Read More
Home Explore Fundamentos de programación - Segunda parte
Publications:
Followers:
Follow
Publications
Read Text Version
More from Ana Paola López Elizondo
P:01

CAPÍTULO 11

Ordenación, búsqueda

y fusión externa (archivos)

11.1. Introducción

11.2. Archivos ordenados

11.3. Fusión de archivos

11.4. Partición de archivos

11.5. Clasificación de archivos

ACTIVIDADES DE PROGRAMACIÓN RESUELTAS

CONCEPTOS CLAVE

RESUMEN

EJERCICIOS

Los sistemas de procesamiento de la información procesan normalmente gran cantidad de información. En

estos casos los datos se almacenan sobre soportes de

almacenamiento masivo (cintas y discos magnéticos).

Los algoritmos de ordenación presentados en el Capítulo 10 no son aplicables si la masa de datos no

cabe en la memoria central de la computadora y se

encuentran almacenados en su soporte como una cinta. En estos casos se suelen colocar en memoria central las fichas que se procesan y a las que se pueda

acceder directamente. Normalmente estas técnicas no

son muy eficaces y se utilizan técnicas distintas de

ordenación. La técnica más importante es la fusión o

mezcla.

Este capítulo realiza una introducción a las técnicas

de ordenación, búsqueda y mezcla o fusión externas.

INTRODUCCIÓN

P:02

406 Fundamentos de programación

11.1. INTRODUCCIÓN

Cuando la masa de datos a procesar es grande y no cabe en la memoria central de la computadora, los datos se organizan en archivos que, a su vez, se almacenan en dispositivos externos de memoria auxiliar (discos, cintas magnéticas, etc.).

Las operaciones básicas estudiadas en el Capítulo 10, ordenación, búsqueda e intercalación o mezcla, sufren un

cambio importante en su concepción, derivado esencialmente del hecho físico de que los datos a procesar no caben

en la memoria principal de la computadora.

11.2. ARCHIVOS ORDENADOS

El tratamiento de los archivos secuenciales exige que éstos se encuentren ordenados respecto a un campo del registro,

denominado campo clave.

Supongamos un archivo del personal de una empresa, cuya estructura de registros es la siguiente:

NOMBRE

DIRECCION

FECHA

SALARIO

CATEGORIA

DNI

tipo cadena

tipo cadena

tipo cadena

tipo numérico

tipo cadena

tipo cadena

(nombre del empleado)

(dirección)

(fecha de nacimiento)

(salario)

(categoría laboral)

(número de DNI)

La clasificación en orden ascendente o descendente se puede realizar con respecto a una clave (nombre,

dirección, etc.). Sin embargo, puede ser interesante tener clasificado un fichero por categoría laboral y a su vez se

puede tener por cada categoría laboral los registros agrupados por nombres o direcciones. Ello nos lleva a la conclusión de que un archivo puede estar ordenado por un campo clave o una jerarquía de campos.

Se dice que un archivo (estructura del registro: campos C1, C2, ... Cn) está ordenado principalmente por el campo C1, en orden secundario 1 por el campo C2, en orden secundario 2 por el campo C3, etc., en orden secundario n

por el campo Cn. Si el archivo tiene la siguiente organización:

• Los registros aparecen en el archivo según el orden de los valores del campo clave C1.

• Si se considera un mismo valor C1, los registros aparecen en el orden de los valores del campo C2.

• Para un mismo valor de C(Ci

) los registros aparecen según el orden de los valores del campo Ci

+ 1, siendo

1 =< i <= n.

Si se desea ordenar un archivo principalmente por C1, y en orden secundario 1 por C2, se necesita:

• Ordenar primero por C2.

• Ejecutar a continuación una ordenación estable por el campo C1.

La mayoría de los Sistemas Operativos actuales disponen de programas estándar (utilidad) que realizan la clasificación de uno o varios archivos (sort). En el caso del Sistema Operativo MS-DOS existe la orden SORT, que permite realizar la clasificación de archivos según ciertos criterios específicos.

Los algoritmos de clasificación externa son muy numerosos y a ellos dedicaremos gran parte de este capítulo.

11.3. FUSIÓN DE ARCHIVOS

La fusión o mezcla de archivos (merge) consiste en reunir en un archivo los registros de dos o más archivos ordenados por un campo clave T. El archivo resultante será un archivo ordenado por el campo clave T.

Supongamos que se dispone de dos archivos ordenados sobre dos cintas magnéticas y que se desean mezclar o

fundir en un solo archivo ordenado. Sean los archivos F1 y F2 almacenados en dos cintas diferentes. El archivo F3

se construye en una tercera cinta.

P:03

Ordenación, búsqueda y fusión externa (archivos) 407

El algoritmo de fusión de archivos será

inicio

//fusión de dos archivos

1. poner archivo 1 en cinta 1, archivo 2 en cinta 2

2. seleccionar de los dos primeros registros de archivo

1 y archivo 2 el registro de clave más pequeña y

almacenarlo en un nuevo archivo 3

3. mientras (archivo 1 no vacio) y (archivo 2 no vacio) hacer

4. seleccionar el registro siguiente con clave mas

pequeña y almacenarlo en el archivo 3

fin_mientras

//uno de los archivos no está aún vacío

5. almacenar resto archivo en archivo 3 registro a registro

fin

EJEMPLO 11.1

Se dispone de dos archivos, F1 y F2, cuyos campos claves son

F1 12 24 36 37 40 52

F2 3 8 9 20

y se desea un archivo FR ordenado, que contenga los dos archivos F1 y F2.

La estructura de los archivos F1 y F2 es:

F1 12 24 36 37 40 52 EOF(*) Fin archivo (eof)

F2 3 8 9 20

Para realizar la fusión de F1 y F2 es preciso acceder a los archivos F1 y F2 que se encuentran en soportes magnéticos en organización secuencial. En cada operación de acceso a un archivo sólo se puede acceder a un único elemento del archivo en un momento dado. Para realizar la operación se utiliza una variable de trabajo del mismo tipo

que los elementos del archivo. Esta variable representa al elemento actual del archivo y denominaremos ventana,

debido a que será la variable que nos permitirá ver el archivo, elemento tras elemento. El archivo se recorre en un

único sentido y su final físico termina con una marca especial denominada fin de archivo (EOF, end of file); por

ejemplo, un asterisco (*).

12

3 8 9 20 *

F1 24 36 37 52 40

Ventana

Ventana

F2

*

Se comparan las claves de las ventanas y se sitúa la más pequeña 3(F2) en el archivo de salida. A continuación,

se avanza un elemento el archivo F2 y se realiza una nueva comparación de los elementos situados en las ventanas.

3 8 9 20 *

Ventana

Ventana

F1 12 24 36 40 52 *

F2

F3 3

P:04

408 Fundamentos de programación

Cuando uno u otro de los archivos de entrada se ha terminado, se copia el resto del archivo sobre el archivo de

salida y el resultado final será:

FR 3 8 9 12 20 24 36 37 40 52 *

El algoritmo correspondiente de fusión de archivos será

algoritmo fusion_archivo

var

entero:ventana1, ventana2, ventanaS

archivo_s de entero: F1,F2,F3

//ventana1,ventana2 claves de los archivos F1,F2

ventanaS claves del archivo FR

inicio

abrir (F1, l, 'nombre')

abrir (F2, l, 'nombre2')

crear (FR, 'nombre3')

abrir (FR, e, 'nombre3')

leer (F1, ventana1)

leer (F2, ventana2)

mientras no FDA(F1) y no FDA (F2) hacer

si ventana1 <= ventana2 entonces

ventanaS ← ventana1

escribir(FR,ventanaS)

leer (F1, ventana1)

si_no

ventanaS ← ventana2

escribir (FR, ventanaS)

leer (F2, ventana2)

fin_si

fin_mientras

//lectura terminada de F1 o F2

mientras no FDA (F1) hacer

ventanaS ← ventana1

escribir(FR, ventanaS)

leer(F1, ventana1)

fin_mientras

mientras_no FDA(F2)hacer

ventanaS ← ventana2

escribir (FR, ventanaS)

leer(F2, ventana2)

fin_mientras

cerrar (F1,F2,FR)

fin

Se considera ahora el otro caso posible en los archivo secuenciales. El final físico del archivo se detecta al leer

el último elemento (no la marca de fin de archivo) y los ficheros son de registros con varios campos. El algoritmo

correspondiente a la fusión sería:

tipo

registro: datos_personales

-: C //campo por el que estan ordenados

-:-

fin_registro

archivo_s de datos_personales: arch

P:05

Ordenación, búsqueda y fusión externa (archivos) 409

var

datos_personales:r1, r2

arch: f1, f2, f //f es el fichero resultante

lógico: fin1, fin2

inicio

abrir (f1, l, 'nombre1')

abrir (f2, l, 'nombre2')

crear (f, 'nombre3')

abrir (f, e, 'nombre3')

fin1← falso

fin2← falso

si FDA (f1) entonces

fin1← verdad

si-no

leer_reg (f1, r1)

fin_si

si FDA (f2) entonces

fin2← verdad

si_no

leer_reg (f2, r2)

fin_si

mientras NO fin1 y NO fin2 hacer

si r1.c < r2.c entonces

escribir_reg (f, r1)

si FDA (f1) entonces

fin1← verdad

si_no

leer_reg (f1, r1)

fin_si

si_no

escribir_reg (f, r2)

si FDA (f2) entonces

fin2← verdad

si_no

leer_reg (f2,r2)

fin_si

fin_si

fin_mientras

mientras NO fin1 hacer

escribir_reg (f, r1)

si FDA (f1) entonces

fin1← verdad

si_no

leer_reg (f1, r1)

fin_si

fin_mientras

mientras NO fin2 hacer

escribir_reg (f, r2)

si FDA (f2) entonces

fin2← verdad

si_no

leer_reg (f2, r2)

fin_si

fin_mientras

cerrar (f1, f2, f)

fin

P:06

410 Fundamentos de programación

11.4. PARTICIÓN DE ARCHIVOS

La partición o división de un archivo consiste en repartir los registros de un archivo en otros dos o más archivos en

función de una determinada condición.

Aunque existen muchos métodos de producir particiones a partir de un archivo no clasificado, consideraremos

sólo los siguientes métodos:

• clasificación interna,

• por el contenido,

• selección por sustitución,

• secuencias.

Supongamos el archivo de entrada siguiente, en el que se indican las claves de los registros:

110 48 33 69 46 2 62 39 28 47 16 19 34 55

99 78 75 40 35 87 10 26 61 92 99 75 11 2

28 16 80 73 18 12 89 50 47 36 67 94 23 15

84 44 53 60 10 39 76 18 24 86

11.4.1. Clasificación interna

El método más sencillo consiste en leer M registros a la vez de un archivo no clasificado, clasificarlos utilizando un

método de clasificación interna y a continuación darles salida como partición. Obsérvese que todas las particiones

producidas de este modo, excepto posiblemente la última, contendrán exactamente M registros. La figura muestra las

particiones producidas a partir del archivo de entrada de la figura utilizada un tamaño de memoria (M) de cinco registros.

33 46 48 69 110

2 28 39 47 62

16 19 34 55 99

35 40 75 78 87

10 26 61 92 99

2 11 16 28 75

12 18 73 80 89

36 47 50 67 94

15 23 44 53 84

10 18 39 60 76

24 86

11.4.2. Partición por contenido

La partición del archivo de entrada se realiza en función del contenido de uno o más campos del registro.

Así, por ejemplo, si se supone un archivo f que se desea dividir en dos archivos f1 y f2, tal que f1 contenga

todos los registros que contengan en el campo clave c, el valor v y en el archivo f2 los restantes registros.

El algoritmo de partición se muestra a continuación:

algoritmo particionå_contenido

....

inicio

abrir (f, l,'nombre')

crear (f1, 'nombre1')

abrir (f1, e, 'nombre1')

P:07

Ordenación, búsqueda y fusión externa (archivos) 411

crear (f2, 'nombre2')

abrir ( f2, e, 'nombre2')

leer(v)

mientras NO FDA (f) hacer

leer_reg (f, r)

si v = r.c entonces

escribir_reg (f1,r)

si_no

escribir_reg (f2, r)

fin_si

fin_mientras

cerrar (f, f1, f2)

fin

11.4.3. Selección por sustitución

La clasificación interna vista en el apartado 11.4.1 no tiene en cuenta la ventaja que puede suponer cualquier ordenación parcial que pueda existir en el archivo de entrada. El algoritmo de selección por sustitución tiene en cuenta

tal ordenación. Los pasos a dar para obtener particiones ordenadas son:

1. Leer N registros del archivo desordenado, poniéndolos todos a no congelados.

2. Obtener el registro R con clave más pequeña de entre los no congelados y escribirlo en partición.

3. Sustituir el registro por el siguiente del archivo de entrada. Este registro se congelará si su clave es más pequeña que la del registro R y no se congelará en otro caso. Si hay registro sin congelar volver al paso 2.

4. Comenzar nueva partición. Si se ha llegado a fin de fichero se repite el proceso sin leer.

Nota: Al final de este método los ficheros con las particiones tienen secuencias ordenadas, lo que no quiere decir

que ambos hayan quedado completamente ordenados.

F: 3 31 14 42 10 15 8 13 63 18 50

F1 F2

3 31 14 42 3 13 50 8 18 8

10 31 14 42 10 13 50 8 18 13

15 31 14 42 14 13 50 8 18 18

15 31 8 42 15 13 50 8 18 50

13 31 8 42 31 13 50 8 18

13 63 8 42 42

13 63 8 18 63

13 50 8 18

algoritmo particion_s

const n= <valor>

tipo

registro: datos_personales

<tipo_dato>:c

...

fin_registro

registro: datos

datos_personales: dp

logico : congela

fin_registro

array[1..n] de datos: arr

archivo_s de datos_personales: arch

var

P:08

412 Fundamentos de programación

datos_personales: r

arr : a

arch : f1,f2, f

lógico : sw

entero : numcongelados, y, posicionmenor

inicio

abrir(f, l, 'nombre')

crear(f1, 'nombre1')

abrir(f1, e, 'nombre1')

crear(f2,'nombre2')

abrir(f2, e, 'nombre32')

numcongelados ← 0

desde i ← 1 hasta n hacer

si no fda(f) entonces

leer_reg(f, r)

a[i].dp ← r

a[i].congela ← falso

si_no

a[i].congela ← verdad

numcongelados ← numcongelados + 1

fin_si

fin_desde

sw ← verdad

mientras no fda(f) hacer

mientras (numcongelados < n) y no fda(f) hacer

buscar_no_congelado_menor(a, posicionmenor)

si sw entonces

escribir_reg(f1, a[posicionmenor].dp)

si_no

escribir_reg(f2, a[posicionmenor].dp)

fin_si

leer_reg(f, r)

si r.c. > a[posicionmenor].dp.c entonces

a[posicionmenor].dp ← r

si_no

a[posicionmenor].dp ← r

a[posicionmenor].congela ← verdad

numcongelados ← numcongelados + 1

fin_si

fin_mientras

sw ← no sw

descongelar(a)

numcongelados ← 0

fin_mientras

mientras numcongelados < n hacer

buscar_no_congelado_menor(a, posicionmenor)

si sw entonces

escribir_reg(f1,a[posicionmenor].dp)

si_no

escribir_reg(f2, a[posicionmenor].dp)

fin_si

a[posicionmenor].congela ← verdad

numcongelados ← numcongelados + 1

fin_mientras

cerrar(f, f1, f2)

fin

P:09

Ordenación, búsqueda y fusión externa (archivos) 413

11.4.4. Partición por secuencias

Los registros se dividen en secuencias alternativas con longitudes iguales o diferentes según los casos.

Las secuencias pueden ser de diferentes diseños:

• El archivo f se divide en dos archivos, f1 y f2, copiando alternativamente en uno y otro archivo secuencias de

registros de longitud m. (Algoritmo particion_1.)

• El archivo f se divide en dos archivos, f1 y f2, de modo que en f1 se copian los registros que ocupan las

posiciones pares y en f2 los registros que ocupan las posiciones impares. (Algoritmo particion_2.)

algoritmo particion_1

tipo

registro: datos personales

<tipodato> : C

..............

fin_registro

archivo_s de datos_personales : arch

var

datos_personales: r

arch : f, f1, f2

lógico : SW

entero : i, n

inicio

abrir (f, l, 'nombre')

crear (f1, 'nombre1')

abrir (f1, e, 'nombre1')

crear (f2, 'nombre2')

abrir (f2, e, 'nombre2')

i← 0

leer (n)

SW← verdad

mientras NO FDA (f) hacer

leer_reg (f,r)

si SW entonces

escribir_reg (f2, r)

si_no

escribir_reg (f2,r)

fin_si

i← i+1

si i = n entonces

SW ← NO SW

i ← 0

fin_si

fin_mientras

cerrar (f, f1, f2)

fin

algoritmo particion_2

tipo

registro: datos_personales

<tipo_dato> : C

...............

fin_registro

archivo_s de datos_personales : arch

var

datos_personales : r

P:10

414 Fundamentos de programación

arch : f1, f2, f

lógico : SW

inicio

abrir (f, l, 'nombre')

crear (f1, 'nombre1')

abrir (f1, e, 'nombre1')

crear (f2, 'nombre2')

abrir (f2, e, 'nombre2')

SW← verdad

mientras NO FDA (f) hacer

leer_reg (f, r)

si SW entonces

escribir_reg (f1,r)

si_no

escribir_reg (f2,r)

fin_si

SW← NO SW

fin_mientras

cerrar (f, f1, f2)

fin

11.5. CLASIFICACIÓN DE ARCHIVOS

Los archivos están clasificados en orden ascendente o descendente cuando todos sus registros están ordenados en

sentido ascendente o descendente respecto al valor de un campo determinado, denominado clave de ordenación.

Si el archivo a ordenar cabe en memoria central, se carga en un vector y se realiza una clasificación interna,

transfiriendo a continuación el archivo ordenado al soporte externo o copiando el resultado en el archivo original si

no se desea conservar.

En el caso de que el archivo no quepa en memoria central, la clasificación se realizará sobre el archivo almacenado en un soporte externo. El inconveniente de este tipo de clasificación reside en el tiempo, que será mucho mayor

debido especialmente a las operaciones entrada/salida de información que requiere la clasificación externa.

Los algoritmos de clasificación son muy variados, pero muchos de ellos se basan en procedimientos mixtos consistentes en aprovechar al máximo la capacidad de la memoria central.

Como métodos de clasificación de archivos que no utilizan la memoria central y son aplicables a archivos secuenciales, se tienen la mezcla directa y la mezcla natural.

11.5.1. Clasificación por mezcla directa

El método más fácil de comprender es el denominado mezcla directa. Se analiza su aplicación a través de un breve

ejemplo en el que se aplicará el método sobre un vector. Se puede pensar en los componentes del vector como las

claves de los registros sucesivos del archivo.

El procedimiento consiste en una partición sucesiva del archivo y una fusión que produce secuencias ordenadas.

La primera partición se hace para secuencias de longitud 1 utilizando dos archivos auxiliares y la fusión producirá secuencias ordenadas de longitud 2. A cada nueva partición y fusión se duplicará la longitud de las secuencias

ordenadas. El método terminará cuando la longitud de la secuencia ordenada exceda la longitud del archivo a ordenar.

Consideremos el archivo:

F: 19 27 2 8 36 5 20 15 6

El archivo F se divide en dos nuevos archivos F1 y F2:

F1: 19 2 36 20 6

F2: 27 8 5 15

P:11

Ordenación, búsqueda y fusión externa (archivos) 415

Ahora se funden los archivos F1 y F2, formando pares ordenados:

F: 19 27 2 8 5 36 15 20 6

Se vuelve a dividir de nuevo en partes iguales y en secuencias de longitud 2

F1: 19 27 5 36 6

F2: 2 8 15 20

La fusión de los archivos producirá

F: 2 8 19 27 5 15 20 36 6

La nueva partición será

F1: 2 8 19 27 6

F2: 5 15 20 36

La nueva fusión será

F: 2 5 8 15 19 20 27 36 6

Cada operación que trata por completo el conjunto de datos en su totalidad se denomina una fase y el proceso de

ordenación se denomina pasada.

F1: 2 5 8 15 19 20 27 36

F2: 6

F: 2 5 6 8 15 19 20 27 36

Evidentemente, la clave de la clasificación es disminuir el número de pasadas e incrementar su tamaño; una secuencia ordenada es una que contiene sólo una pasada que, a su vez, contiene todos los elementos de la pasada.

EJEMPLO 11.2

Para la implementación de los siguientes algoritmos no se consideró la existencia de un registro especial que indicara el fin de archivo. La función FDA(id_arch) retorna cierto cuando se accede al último registro.

Si se considerase la existencia del registro especial que marca el fin de archivo se podría prescindir del uso de

las variables lógicas fin, fin1, fin2.

algoritmo ord_mezcla_directa

...

procedimiento ordmezcladirecta

var

datos_personales: r,r1,r2

arch : f,f1,f2

// El tipo arch es archivo_s de datos_personales

entero : lgtud, long

lógico : sw,fin1,fin2

entero : i,j

inicio

// calcularlongitud(f) es una función definida por el usuario que

// devuelve el número de registros del archivo original

P:12

416 Fundamentos de programación

long ← calcularlogitud(f)

lgtud ← 1

mientras lgtud < long hacer

abrir(f,l,'fd')

crear(f1,'f1d')

crear(f2,'f2d')

abrir(f1,e,'f1d')

abrir(f2,e,'f2d')

i ← 0

sw ← verdad

mientras no FDA(f) hacer

leer_reg(f,r)

si sw entonces

escribir_reg(f1,r)

si_no

escribir_reg(f2,r)

fin_si

i ← i + 1

si i=lgtud entonces

sw ← no sw

i ← 0

fin_si

fin_mientras

cerrar(f,f1,f2)

abrir(f1,l,'f1d')

abrir(f2,l,'f2d')

crear(f,'fd')

abrir (f,e,'fd')

i ← 0

j ← 0

fin1 ← falso

fin2 ← falso

si FDA(f1) entonces

fin1 ← verdad

si_no

leer_reg(f1,r1)

fin_si

si FDA(f2) entonces

fin2 ← verdad

si_no

leer_reg(f2,r2)

fin_si

mientras no fin1 o no fin2 hacer

mientras no fin1 y no fin2 y (i<lgtud) y (j<lgtud) hacer

si menor(r1,r2) entonces

escribir_reg(f,r1)

si FDA(f1) entonces

fin1 ← verdad

si_no

leer_reg(f1,r1)

fin_si

i ← i + 1

si_no

escribir_reg(f,r2)

si FDA(f2) entonces

fin2 ← verdad

P:13

Ordenación, búsqueda y fusión externa (archivos) 417

si_no

leer_reg(f2,r2)

fin_si

j ← j + 1

fin_si

fin_mientras

mientras no fin1 y (i < lgtud) hacer

escribir_reg(f,r1)

si FDA(f1) entonces

fin1 ← verdad

si_no

leer_reg(f1,r1)

fin_si

i ← i + 1

fin_mientras

mientras no fin2 y (j < lgtud) hacer

escribir_reg(f,r2)

si FDA(f2) entonces

fin2 ← verdad

si_no

leer_reg(f2,r2)

fin_si

j ← j + 1

fin_mientras

i ← 0

j ← 0

fin_mientras // del mientras no fin1 o no fin2

cerrar(f,f1,f2)

lgtud ← lgtud*2

fin_mientras // del mientras lgtud < long

borrar('f1d')

borrar('f2d')

fin_procedimiento

11.5.2. Clasificación por mezcla natural

Es uno de los mejores métodos de ordenación de ficheros secuenciales. Consiste en aprovechar la posible ordenación

interna de las secuencias del archivo (F), obteniendo con ellas particiones ordenadas de longitud variable sobre una

serie de archivos auxiliares, en este caso dos, F1 y F2. A partir de estos ficheros auxiliares se escribe un nuevo F

mezclando los segmentos crecientes máximos de cada uno de ellos.

EJEMPLO 11.3

Clasificar el vector

F: 19 27 2 8 36 5 20 15 6

Se divide F en dos vectores F1 y F2, donde se ponen alternativamente los elementos F1 y F2. F está ahora vacío.

Etapa 1, fase 1:

F1: 19 27/ 5 20/ 6

F2: 2 8 36/ 15

Se selecciona el elemento más pequeño de F1 y F2, que pasan a estar en F3.

P:14

418 Fundamentos de programación

Etapa 1, fase 2:

F1: 19 27/ 5 20/ 6

F2: 8 36/ 15

F3: 2

Ahora se comparan 8 y 19, se selecciona 8. De modo similar, 19 y 27:

F1: 5 20/ 6

F2: 36/ 15

F3: 2 8 19 27

En F1 se ha interrumpido la secuencia creciente y se continúa con F2 hasta que también en él se termine la secuencia creciente.

F1: 5 20/ 6

F2: 15

F3: 2 8 19 27 36

Ahora 5 y 15 son menores que 36. Finalmente se tendrá

F3: 2 8 19 27 36/ 5 15 20/ 6

F1 y F2 están ahora vacíos.

Etapa 2, fase 1:

Dividir F3 en dos

F1: 2 8 19 27 36/ 6

F2: 5 15 20

Etapa 2, fase 2:

Se mezclan F1 y F2

F3: 2 5 8 15 19 20 27 36 6

Etapa 3, fase 1:

F1: 2 5 8 15 19 20 27 36

F2: 6

Etapa 3, fase 2:

F3: 2 5 6 8 15 19 20 27 36

y el archivo F3 ya está ordenado.

Algoritmo

algoritmo ord_mezcla_natural

...

procedimiento ordmezclanatural

var

datos_personales: r,r1,r2,ant,ant1,ant2

arch : f,f1,f2

//El tipo arch es archivo_s de datos_personales

P:15

Ordenación, búsqueda y fusión externa (archivos) 419

lógico : ordenado,crece,fin,fin1,fin2

entero : numsec

inicio

ordenado ← falso

mientras no ordenado hacer

// Partir

abrir(f,l,'fd')

crear(f1,'f1d')

crear(f2,'f2d');

abrir(f1,e,'f1d')

abrir(f2,e,'f2d')

fin ← falso

si FDA(f) entonces

fin ← verdad

si_no

leer_reg(f,r)

fin_si

mientras no fin hacer

ant ← r

crece ← verdad

mientras crece y no fin hacer

si menorigual(ant,r) entonces

escribir_reg(f1,r)

ant ← r

si FDA(f) entonces

fin ← verdad

si_no

leer_reg(f,r)

fin_si

si_no

crece ← falso

fin_si

fin mientras

ant ← r

crece ← verdad

mientras crece y no fin hacer

si menorigual(ant,r) entonces

escribir_reg(f2,r)

ant ← r

si FDA(f) entonces

fin ← verdad

si_no

leer_reg(f,r)

fin_si

si_no

crece ← falso

fin_si

fin_mientras

fin_mientras

cerrar(f,f1,f2)

//Mezclar

abrir(f1,l,'f1d')

abrir(f2,l.'f2d')

crear(f,'fd')

abrir(f,e,'fd')

fin1 ← falso

P:16

420 Fundamentos de programación

fin2 ← falso

si FDA(f1) entonces

fin1 ← verdad

si_no

leer_reg(f1,r1)

fin_si

si FDA(f2) entonces

fin2 ← verdad

si_no

leer_reg(f2,r2)

fin_si

numsec ← 0

mientras NO fin1 y NO fin2 hacer

ant1 ← r1

ant2 ← r2

crece ← verdad

mientras NO fin1 y NO fin2 y crece hacer

si menorigual(ant1,r1) y menorigual(ant2,r2) entonces

si menorigual(r1,r2) entonces

escribir_reg(f,r1)

ant1 ← r1

si FDA(f1) entonces

fin1 ← verdad

si_no

leer_reg(f1,r1)

fin_si

si_no

escribir_reg(f,r2)

ant2 ← r2

si FDA(f2) entonces

fin2 ← verdad

si_no

leer_reg(f2,r2)

fin_si

fin_si

si_no

crece ← falso

fin_si

fin_mientras

mientras NO fin1 y menorigual(ant1,r1) hacer

escribir_reg(f,r1)

ant1 ← r1

si FDA(f1) entonces

fin1 ← verdad

si_no

leer_reg(f1,r1)

fin_si

fin_mientras

mientras NO fin2 y menorigual(ant2,r2) hacer

escribir_reg(f,r2)

ant2 ← r2

si FDA(f2) entonces

fin2 ← verdad

si_no

leer_reg(f2,r2)

fin_si

P:17

Ordenación, búsqueda y fusión externa (archivos) 421

fin_mientras

numsec ← numsec + 1

fin_mientras // del mientras no fin1 y no fin2

si NO fin1 entonces

numsec ← numsec+1

mientras NO fin1 hacer

escribir_reg(f,r1)

si FDA(f1) entonces

fin1 ← verdad

si_no

leer_reg(f1,r1)

fin_si

fin_mientras

fin_si

si no fin2 entonces

numsec ← numsec+1

mientras no fin2 hacer

escribir_reg(f,r2)

si FDA(f2) entonces

fin2 ← verdad

si_no

leer_reg(f2,r2)

fin_si

fin_mientras

fin_si

cerrar(f,f1,f2)

si numsec <= 1 entonces

ordenado ← verdad

fin_si

fin_mientras // del mientras no ordenado

borrar('f1d')

borrar('f2d')

fin_procedimiento

11.5.3. Clasificación por mezcla de secuencias equilibradas

Este método utiliza la memoria de la computadora para realizar clasificaciones internas y cuatro archivos secuenciales temporales para trabajar.

Supóngase un archivo de entrada F que se desea ordenar por orden creciente de las claves de sus elementos. Se

dispone de cuatro archivos secuenciales de trabajo, F1, F2, F3 y F4, y que se pueden colocar m elementos en memoria central en un momento dado en una tabla T de m elementos. El proceso es el siguiente:

1. Lectura de archivo de entrada por bloques de n elementos.

2. Ordenación de cada uno de estos bloques y escritura alternativa sobre F1 y F2.

3. Fusión de F1 y F2 en bloques de 2n elementos que se escriben alternativamente sobre F3 y F4.

4. Fusión de F3 y F4 y escritura alternativa en F1 y F2, de bloques con 4n elementos ordenados.

5. El proceso consiste en doblar cada vez el tamaño de los bloques y utilizando las parejas (F1, F2) y (F3, F4).

Fichero de entrada

46 66 4 12 7 5 34 32 68 8 99 16 13 14 12 10

F1 4 12 46 66 8 16 68 99

F2 [5 7 32 34] [10 12 13 14]

F3 vacio

F4 vacio

P:18

422 Fundamentos de programación

Fusión por bloques

F1 vacío

F2 vacío

F3 4 5 7 12 32 34 46 66 /F4 8 10 12 13 14 16 68 99

La mezcla o fusión final es

F1 4 5 7 8 10 12 12 13 14 16 32 34 46 66 68 99

F2 vacío

F3 vacío

F4 vacío

ACTIVIDADES DE PROGRAMACIÓN RESUELTAS

11.1. Realizar el algoritmo de partición de un archivo F en dos particiones F1 y F2, según el contenido de un campo

clave C. El contenido debe tener el valor v.

algoritmo Partición_contenido

...

inicio

abrir(f, l, 'f0') //lectura

crear (f1, 'f1')

crear (f2, 'f2')

abrir (f1, e, 'f1') //escritura

abrir (f2, e, 'f2') //escritura

mientras no fda(f) hacer

leer_reg(f, r)

si r.c = v entonces

escribir_reg(f1, r)

si_no

escribir_reg(f2, r)

fin_si

fin_mientras

cerrar( f1, f2, f)

fin

11.2. Realizar el algoritmo de partición por secuencias alternativas de longitud n.

• Entrada: Archivo F

• Salida: Archivos F1, F2

• Secuencia: longitud n (n registros en cada secuencia)

• Cada n registros se almacenan alternativamente en F1 y F2.

algoritmo Partir_alternativa

tipo

registro: reg

...

fin_registro

archivo_s de reg : arch

var

logico: sw

entero: i //contador de elementos de la secuencia

arch: f, f1, f2

reg: r

inicio

abrir(f, l, 'f0') //lectura

crear (f1, 'f1')

P:19

Ordenación, búsqueda y fusión externa (archivos) 423

crear (f2, 'f2')

abrir (f1, e, 'f1') //escritura

abrir (f2, e, 'f2') //escritura

sw ← falso

i ← 0

mientras no fda(f) hacer

leer_reg(f, r)

si NO sw entonces

escribir_reg(f1, r)

si_no

escribir_reg(f2, r)

fin_si

i ← i+1

si i= n entonces

sw ← verdadero

i ← 0 //se inicializa el contador de la secuencia

fin_si

fin_mientras

cerrar(f, f1, f2)

fin

11.3. Aplicar el algoritmo de mezcla directa al archivo F de claves.

F: 9 7 2 8 16 15 2 10

1. Primera división (partición en secuencias de longitud 1)

F1: 9 2 16 2

F2: 7 8 15 10

2. Mezcla de F1 y F2, formando pares ordenados

F: 7 9 || 2 8 || 15 16 || 2 10

3. Segunda división (partición en secuencias de longitud 2)

F1: 7 9 || 15 16

F2: 2 8 || 2 10

4. Mezcla de F1 y F2

F: 2 7 8 9 || 2 10 15 16

5. Tercera división (partición en secuencias de longitud 4)

F1: 2 7 8 9

F2: 2 10 15 16

6. Mezcla de F1 y F2 (última)

F: 2 2 7 8 9 10 15 16

11.4. Escribir el procedimiento de mezcla de dos archivos ordenados en secuencias de una determinada longitud.

procedimiento fusion (E cadena: nombre1, nombre2, nombre3; E entero: lgtud)

var

arch : f1, f2, f

//el tipo arch se supone definido en el programa principal

reg: r1, r2

// el tipo reg se supone definido en el programa principal

entero : i, j

lógico : fin1, fin2

inicio

{los nombres de los archivos en el dispositivo de almacenamiento se

P:20

424 Fundamentos de programación

pasan al procedimiento de fusión a través de las variables nombre1,

nombre2 y nombre3 }

abrir(f1,l, nombre1)

abrir(f2,l, nombre2)

crear(f, nombre3)

abrir (f, e, nombre3)

leerRegYFin(f1, r1, fin1)

{ leerRegYFin es un procedimiento, desarrollado más adelante, que

lee un registro y detecta la marca de fin de archivo}

leerRegYFin(f2, r2, fin2)

mientras no fin1 o no fin2 hacer

i ← 0

j ← 0

mientras no fin1 y no fin2 y (i<lgtud) y (j<lgtud) hacer

//lgtud es la longitud de la secuencia recibida como parámetro

si menor(r1,r2) entonces

escribir_reg(f,r1)

leerRegYFin(f1, r1, fin1)

i ← i+1

si_no

escribir_reg(f,r2)

leerRegYFin(f2, r2, fin2)

j ← j+1

fin_si

fin_mientras

mientras no fin1 y (i < lgtud) hacer

escribir_reg(f,r1)

leerRegYFin(f1, r1, fin1)

i ← i+1

fin_mientras

mientras no fin2 y (j < lgtud) hacer

escribir_reg(f,r2)

leerRegYFin(f2, r2, fin2)

j ← j+1

fin_mientras

fin_mientras // del mientras no fin1 o no fin2

cerrar(f,f1,f2)

fin_procedimiento

procedimiento leerRegYFin(E/S arch: f; E/S reg: r; E/S lógico: fin)

inicio

si fda(f) entonces

fin ← verdad

si_no

leer_reg(f, r)

fin_si

fin_procedimiento

11.5. Escribir el procedimiento de ordenación por mezcla directa de un archivo con long registros, utilizando el procedimiento fusión del ejercicio anterior.

procedimiento ordenarDirecta(E cadena: nombref, nombref1, nombref2;

E entero: long)

var

entero: lgtud

inicio

lgtud ← 1

mientras lgtud <= long hacer

partirAlternativoEnSec (nombref, nombref1, nombref2, lgtud)

fusion(nombref1, nombref2, nombref, lgtud)

lgtud ← lgtud * 2

P:21

Ordenación, búsqueda y fusión externa (archivos) 425

fin_mientras

borrar(nombref1)

borrar(nombref2)

fin_procedimiento

procedimiento partirAlternativoEnSec (E cadena: nombref, nombref1,

nombref2; E entero: lgtud)

var

lógico: sw

entero: i // contador de elementos de la secuencia

arch : f1, f2, f // tipo definido en el programa principal

reg: r // tipo definido en el programa principal

inicio

abrir(f, l, nombref) //lectura

crear (f1, nombref1)

crear (f2, nombref2)

abrir (f1, e, nombref1) //escritura

abrir (f2, e, nombref2) //escritura

sw ← falso

i ← 0

mientras no fda(f) hacer

leer_reg(f, r)

si NO sw entonces

escribir_reg(f1, r)

si_no

escribir_reg(f2, r)

fin_si

i ← i+1

si i= lgtud entonces

sw ← verdadero

i ← 0 //se inicializa el contador de la secuencia

fin_si

fin_mientras

cerrar(f, f1, f2)

fin

11.6. Escribir el procedimiento de ordenación por mezcla natural de un archivo, utilizando los procedimientos auxiliares

partir y mezclar que se suponen implementados. El procedimiento partir aprovecha las secuencias ordenadas

que pudieran existir en el archivo original y las coloca alternativamente sobre dos archivos auxiliares. El procedimiento mezclar construye a partir de dos ficheros auxiliares un nuevo fichero, mezclando las secuencias crecientes

que encuentra en los ficheros auxiliares para construir sobre el destino secuencias crecientes de longitud mayor.

procedimiento ordenarNatural(E cadena: nombref, nombref1, nombref2)

var

entero: numsec

lógico: ordenado

inicio

ordenado ← falso

mientras NO ordenado hacer

partir (nombref, nombref1, nombref2)

numsec ← 0

mezclar(nombref1, nombref2, nombref, numsec)

si numsec <=1 entonces

ordenado ← verdad

fin_si

fin_mientras

borrar(nombref1)

borrar(nombref2)

fin_procedimiento

P:22

426 Fundamentos de programación

CONCEPTOS CLAVE

• Mezcla.

• Mezcla directa.

• Mezcla natural.

• Ordenación externa.

• Partición.

RESUMEN

La ordenación externa se emplea cuando la masa de datos

a procesar es grande y no cabe en la memoria central de la

computadora. Si el archivo es directo, aunque los registros

se encuentran colocados en él de forma secuencial, servirá

cualquiera de los métodos de clasificación vistos como métodos de ordenación interna, con ligeras modificaciones

debido a las operaciones de lectura y escritura de registros

en el disco. Si el archivo es secuencial, es necesario emplear otros métodos basados en procesos de partición y

medida.

1. La partición es el proceso por el cual los registros

de un archivo se reparten en otros dos o más archivos en función de una condición.

2. La fusión o mezcla consiste en reunir en un archivo los registros de dos o más. Habitual mente los

registros de los archivos originales se encuentran

ordenados por un campo clave, y la mezcla ha de

efectuarse de tal forma que se obtenga un archivo

ordenado por dicho campo clave.

3. Los archivos están clasificados en orden ascendente o descendente cuando todos sus registros

están ordenados en sentido ascendente o descendente respecto al valor de un campo determinado,

denominado clave de ordenación. Los algoritmos

de clasificación son muy variados: (1) si el archivo a ordenar cabe en memoria central, se carga

en un vector y se realiza una clasificación interna,

transfiriendo a continuación el archivo ordenado

al soporte externo; (2) si el archivo a ordenar

no cabe en memoria central y es secuencial son

aplicables la mezcla directa y la mezcla natural;

(3) si no es secuencial pueden aplicarse métodos

similares a los vistos en la clasificación interna

con ligeras modificaciones; (4) otros métodos se

basan en procedimientos mixtos consistentes en

aprovechar al máximo la capacidad de la memoria

central.

4. La clasificación por mezcla directa consiste en

una partición sucesiva del archivo y una fusión

que produce secuencias ordenadas. La primera

partición se hace para secuencias de longitud 1

y la fusión producirá secuencias ordenadas de

longitud 2. A cada nueva partición y fusión se

duplicará la longitud de las secuencias ordenadas.

El método terminará cuando la longitud de la secuencia ordenada exceda la longitud del archivo

a ordenar.

5. La clasificación por mezcla natural consiste en

aprovechar la posible ordenación interna de las

secuencias del archivo original (F), obteniendo

con ellas particiones ordenadas de longitud variable sobre los ficheros auxiliares. A partir de

estos ficheros auxiliares escribiremos un nuevo F

mezclando los segmentos crecientes de cada uno

de ellos.

6. La búsqueda es el proceso de localizar un registro

en un archivo con un determinado valor en uno de

sus campos. Los archivos de tipo secuencial obligan a efectuar búsquedas secuenciales, mientras

que los archivos directos son estructuras de acceso

aleatorio y permiten otros tipos de búsquedas.

7. La búsqueda binaria podría aplicarse a archivos

directos con los registros colocados uno a continuación de otro y ordenados por el campo por el

que se desea efectuar la búsqueda.

P:23

Ordenación, búsqueda y fusión externa (archivos) 427

EJERCICIOS

11.1. Se desea intercalar los registros del archivo P con los

registros del archivo Q y grabarlos en otro archivo R.

NOTA: Los archivos P y Q están clasificados en orden ascendente por una determinada clave y se desea

que el archivo R quede también ordenado en modo

ascendente.

11.2. Los archivos M, N y P contienen todas las operaciones

de ventas de una empresa en los años 1985, 1986 y

1987 respectivamente. Se desea un algoritmo que

intercale los registros de los tres archivos en un solo

archivo Z, teniendo en cuenta que los tres archivos

están clasificados en orden ascendente por el campo

clave ventas.

11.3. Se dispone de dos archivos secuenciales F1 y F2 que

contienen cada uno de ellos los mismos campos. Los

dos archivos están ordenados de modo ascendente

por el campo clave (alfanumérico) y existen registros

comunes a ambos archivos. Se desea diseñar un programa que obtenga: a) un archivo C a partir de F1 y

F2, que contenga todos los registros comunes, pero

sólo una vez; b) un archivo que contenga todos los

registros que no son comunes a F1 y F2.

11.4. Se desea intercalar los registros del archivo A con los

registros del archivo B y grabarlos en un tercer archivo C. Los archivos A y B están clasificados en

orden ascendente por su campo clave. Y se desea

también que el archivo C quede clasificado en orden

ascendente.

11.5. El archivo A contiene los números de socios del Club

Deportivo Esmeralda y el archivo B los códigos de

los socios del Club Deportivo Diamante. Se desea

crear un archivo C que contenga los números de los

socios que pertenecen a ambos clubes. Asimismo, se

desea saber cuántos registros se han leído y cuántos

se han grabado.

11.6. Los archivos F1, F2 y F3 contienen todas las operaciones de ventas de una compañía informática en los

años 1985, 1986 y 1987 respectivamente. Se desea un

programa que intercale todos los registros de los tres

archivos en un solo archivo F, suponiendo que todo

registro posee un campo clave y que F1, F2 y F3 están

clasificados en orden ascendente de ese campo clave.

11.7. Se desea actualizar un archivo maestro de la nómina

de la compañía Aguas del Pacífico con un archivo

MODIFICACIONES que contiene todas las incidencias

de empleados (altas, bajas, modificaciones). Ambos

archivos están clasificados en orden ascendente del

código de empleado (campo clave). El nuevo archivo

maestro actualizado debe conservar la clasificación

ascendente por código de empleado y sólo debe existir un registro por empleado.

11.8. Se tiene un archivo maestro de inventarios con los

siguientes campos:

CODIGO DE ARTICULO DESCRIPCION

EXISTENCIAS

Se desea actualizar el archivo maestro con los movimientos habidos durante el mes (altas/bajas). Para

ello se incluyen los movimientos en un archivo OPERACIONES que contiene los siguientes campos:

CODIGO DE ARTICULO CANTIDAD

OPERACION (1-Alta, 2-Baja)

Los dos archivos están clasificados por el mismo

campo clave.

P:25

CAPÍTULO 12

Estructuras dinámicas

lineales de datos

(pilas, colas y listas enlazadas)

12.1. Introducción a las estructuras de datos

12.2. Listas

12.3. Listas enlazadas

12.4. Procesamiento de listas enlazadas

12.5. Listas circulares

12.6. Listas doblemente enlazadas

12.7. Pilas

12.8. Colas

12.9. Doble cola

ACTIVIDADES DE PROGRAMACIÓN RESUELTAS

CONCEPTOS CLAVE

RESUMEN

EJERCICIOS

Los datos estudiados hasta ahora se denominan estáticos. Ello es debido a que las variables son direcciones

simbólicas de posiciones de memoria; esta relación

entre nombres de variables y posiciones de memoria

es una relación estática que se establece por la declaración de las variables de una unidad de programa y

que se establece durante la ejecución de esa unidad.

Aunque el contenido de una posición de memoria

asociada con una variable puede cambiar durante la

ejecución, es decir, el valor de la variable puede cambiar, las variables por sí mismas no se pueden crear ni

destruir durante la ejecución. En consecuencia, las variables consideradas hasta este punto se denominan

variables estáticas.

En algunas ocasiones, sin embargo, no se conoce

por adelantado cuánta memoria se requerirá para un

programa. En esos casos es conveniente disponer de

un método para adquirir posiciones adicionales de

memoria a medida que se necesiten durante la ejecución del programa y liberarlas cuando no se necesitan.

Las variables que se crean y están disponibles durante

la ejecución de un programa se llaman variables dinámicas. Estas variables se representan con un tipo de

datos conocido como puntero. Las variables dinámicas

se utilizan para crear estructuras dinámicas de datos

que se pueden ampliar y comprimir a medida que se

requieran durante la ejecución del programa. Una estructura de datos dinámica es una colección de elementos denominados nodos de la estructura —normalmente de tipo registro— que son enlazados juntos.

Las estructuras dinámicas de datos se clasifican en

lineales y no lineales. El estudio de las estructuras lineales, listas, pilas y colas, es el objetivo de este capítulo.

INTRODUCCIÓN

P:26

430 Fundamentos de programación

12.1. INTRODUCCIÓN A LAS ESTRUCTURAS DE DATOS

En capítulos anteriores se ha introducido a las estructuras de datos, definiendo tipos y estructuras de datos primitivos,

tales como enteros, real y carácter, utilizados para construir tipos más complicados como arrays y registros, denominados estructuras de datos compuestos. Tienen una estructura porque sus datos están relacionados entre sí. Las

estructuras compuestas, tales como arrays y registros, están soportadas en la mayoría de los lenguajes de programación, debido a que son necesarias en casi todas las aplicaciones.

La potencia y flexibilidad de un lenguaje está directamente relacionada con las estructuras de datos que posee.

La programación de algoritmos complicados puede resultar muy difícil en un lenguaje con estructuras de datos limitados, caso de FORTRAN y COBOL. En ese caso es conveniente pensar en la implementación con lenguajes que

soporten punteros como C y C++ o bien que no soporten pero tengan recolección de basura como Java o C#, o bien

recurrir, al menos en el período de formación, al clásico Pascal.

Cuando una aplicación particular requiere una estructura de datos no soportada por el lenguaje, se hace necesaria

una labor de programación para representarla. Se dice que necesitamos implementar la estructura de datos. Esto naturalmente significa más trabajo para el programador. Si la programación no se hace bien, se puede malgastar tiempo de programación y —naturalmente— de computadora. Por ejemplo, supongamos que tenemos un lenguaje como

Pascal que permite arrays de una dimensión de números enteros y reales, pero no arrays multidimensionales. Para

implementar una tabla con cinco filas y diez columnas podemos utilizar

type

array[0..10] of real: FILA;

var

FILA: FILA1, FILA2, FILA3, FILA4, FILA5;

La llamada al elemento de la tercera fila y sexta columna se realizará con la instrucción

FILA3 [6]

Un método muy eficaz es diseñar procedimientos y funciones que ejecuten las operaciones realizadas por las

estructuras de datos. Sin embargo, con las estructuras vistas hasta ahora arrays y registros tienen dos inconvenientes:

1) la reorganización de una lista, si ésta implica movimiento de muchos elementos de datos, puede ser muy costosa,

y 2) son estructuras de datos estáticas.

Una estructura de datos se dice que es estática cuando el tamaño ocupado en memoria es fijo, es decir, siempre

ocupa la misma cantidad de espacio en memoria. Por consiguiente, si se representa una lista como vector, se debe

anticipar (declarar o dimensionar) la longitud de esa lista cuando se escribe un programa; es imposible ampliar el

espacio de memoria disponible (algunos lenguajes permiten dimensionar dinámicamente el tamaño de un array durante la ejecución del programa, como es el caso de Visual BASIC). En consecuencia, puede resultar difícil representar diferentes estructuras de datos.

Los arrays unidimensionales son estructuras estáticas lineales ordenadas secuencialmente. Las estructuras se convierten en dinámicas cuando los elementos pueden ser insertados o suprimidos directamente sin necesidad de algoritmos complejos. Se distinguen las estructuras dinámicas de las estáticas por los modos en que se realizan las inserciones y borrados de elementos.

12.1.1. Estructuras dinámicas de datos

Las estructuras dinámicas de datos son estructuras que «crecen a medida que se ejecuta un programa». Una estructura dinámica de datos es una colección de elementos —llamados nodos— que son normalmente registros. Al contrario que un array, que contiene espacio para almacenar un número fijo de elementos, una estructura dinámica de

datos se amplía y contrae durante la ejecución del programa, basada en los registros de almacenamiento de datos del

programa.

Las estructuras dinámicas de datos se pueden dividir en dos grandes grupos:

lineales { pilas

colas

listas enlazadas

P:27

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 431

no lineales { árboles

grafos

Las estructuras dinámicas de datos se utilizan para almacenamiento de datos del mundo real que están cambiando constantemente. Un ejemplo típico ya lo hemos visto como estructura estática de datos: la lista de pasajeros de una línea aérea. Si esta lista se mantuviera en orden alfabético en un array, sería necesario hacer espacio

para insertar un nuevo pasajero por orden alfabético. Esto requiere utilizar un bucle para copiar los datos del registro de cada pasajero en el siguiente elemento del array. Si en su lugar se utilizara una estructura dinámica de

datos, los nuevos datos del pasajero se pueden insertar simplemente entre dos registros existentes sin un mínimo

esfuerzo.

Las estructuras dinámicas de datos son extremadamente flexibles. Como se ha descrito anteriormente, es relativamente fácil añadir nueva información creando un nuevo nodo e insertándolo entre nodos existentes. Se verá que es

también relativamente fácil modificar estructuras dinámicas de datos, eliminando o borrando un nodo existente.

En este capítulo examinaremos las tres estructuras dinámicas lineales de datos: listas, colas y pilas, dejando para

el próximo capítulo las estructuras no lineales de datos: árboles y grafos.

Una estructura estática de datos es aquella cuya estructura se especifica en el momento en que se escribe el

programa y no puede ser modificada por el programa. Los valores de sus diferentes elementos pueden variar,

pero no su estructura, ya que ésta es fija.

Una estructura dinámica de datos puede modificar su estructura mediante el programa. Puede ampliar o limitar

su tamaño mientras se ejecuta el programa.

12.2. LISTAS

Una lista lineal es un conjunto de elementos de un tipo dado que pueden variar en número y donde cada elemento

tiene un único predecesor y un único sucesor o siguiente, excepto el primero y último de la lista. Esta es una definición muy general que incluye los ficheros y vectores.

Los elementos de una lista lineal se almacenan normalmente contiguos —un elemento detrás de otro— en posiciones consecutivas de la memoria. Las sucesivas entradas en una guía o directorio telefónico, por ejemplo, están en

líneas sucesivas, excepto en las partes superior e inferior de cada columna. Una lista lineal se almacena en la memoria principal de una computadora en posiciones sucesivas de memoria; cuando se almacenan en cinta magnética,

los elementos sucesivos se presentan en sucesión en la cinta. Esta asignación de memoria se denomina almacenamiento secuencial. Posteriormente se verá que existe otro tipo de almacenamiento denominado encadenado o enlazado.

Las líneas así definidas se denominan contiguas. Las operaciones que se pueden realizar con listas lineales contiguas son:

1. Insertar, eliminar o localizar un elemento.

2. Determinar el tamaño —número de elementos— de la lista.

3. Recorrer la lista para localizar un determinado elemento.

4. Clasificar los elementos de la lista en orden ascendente o descendente.

5. Unir dos o más listas en una sola.

6. Dividir una lista en varias sublistas.

7. Copiar la lista.

8. Borrar la lista.

Una lista lineal contigua se almacena en la memoria de la computadora en posiciones sucesivas o adyacentes y

se procesa como un array unidimensional. En este caso, el acceso a cualquier elemento de la lista y la adición de

nuevos elementos es fácil; sin embargo, la inserción o borrado requiere un desplazamiento de lugar de los elementos

que le siguen y, en consecuencia, el diseño de un algoritmo específico.

Para permitir operaciones con listas como arrays se deben dimensionar éstos con tamaño suficiente para que

contengan todos los posibles elementos de la lista.

P:28

432 Fundamentos de programación

EJEMPLO 12.1

Se desea leer el elemento j-ésimo de una lista P.

El algoritmo requiere conocer el número de elementos de la lista (su longitud, L). Los pasos a dar son:

1. conocer longitud de la lista L.

2. si L = 0 visualizar «error lista vacía».

si_no comprobar si el elemento j-ésimo está dentro del rango permitido de elementos 1 <= j <= L; en este

caso, asignar el valor del elemento P(j) a una variable B; si el elemento j-ésimo no está dentro del rango,

visualizar un mensaje de error «elemento solicitado no existe en la lista».

3. fin.

El pseudocódigo correspondiente sería:

procedimiento acceso(E lista: P; S elementolista: B; E entero: L, J)

inicio

si L = 0 entonces

escribir('Lista vacia')

si_no

si (j >= 1) y (j <= L) entonces

B ← P[j]

si_no

escribir('ERROR: elemento no existente')

fin_si

fin_si

fin

EJEMPLO 12.2

Borrar un elemento j de la lista P.

Variables

L longitud de la lista

J posición del elemento a borrar

I subíndice del array P

P lista

Las operaciones necesarias son:

1. Comprobar si la lista es vacía.

2. Comprobar si el valor de J está en el rango I de la lista 1 <= J <= L.

3. En caso de J correcto, mover los elementos J+1, J+2, ..., a las posiciones J, J+1, ..., respectivamente,

con lo que se habrá borrado el antiguo elemento J.

4. Decrementar en uno el valor de la variable L, ya que la lista contendrá ahora L – 1 elementos.

El algoritmo correspondiente será:

inicio

si L = 0 entonces

escribir('lista vacia')

si_no

leer(J)

si (J >= 1) y (J <= L) entonces

desde I ← J hasta L-1 hacer

P[I] ← P[I+1]

fin_desde

P:29

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 433

L ← L-1

si_no

escribir('Elemento no existe')

fin_si

fin_si

fin

Una lista contigua es aquella cuyos elementos son adyacentes en la memoria o soporte direccionable. Tiene unos

límites izquierdo y derecho o inferior/superior que no pueden ser rebajados cuando se le añade un elemento.

La inserción o eliminación de un elemento, excepto en la cabecera o final de la lista, necesita una traslación

de una parte de los elementos de la misma: la que precede o sigue a la posición del elemento modificado.

Las operaciones directas de añadir y eliminar se efectúan únicamente en los extremos de la lista. Esta limitación es una de las razones por las que esta estructura es poco utilizada.

Las listas enlazadas o de almacenamiento enlazado o encadenado son mucho más flexibles y potentes, y su uso

es mucho más amplio que las listas contiguas.

12.3. LISTAS ENLAZADAS1

Los inconvenientes de las listas contiguas se eliminan con las listas enlazadas. Se pueden almacenar los elementos

de una lista lineal en posiciones de memoria que no sean contiguas o adyacentes.

Una lista enlazada o encadenada es un conjunto de elementos en los que cada elemento contiene la posición

—o dirección— del siguiente elemento de la lista. Cada elemento de la lista enlazada debe tener al menos dos campos: un campo que tiene el valor del elemento y un campo (enlace, link) que contiene la posición del siguiente elemento, es decir, su conexión, enlace o encadenamiento. Los elementos de una lista son enlazados por medio de los

campos enlaces.

Las listas enlazadas tienen una terminología propia que se suele utilizar normalmente. Primero, los valores se

almacenan en un nodo (Figura 12.1).

Dato

(valor elemento)

Enlace

Figura 12.1. Nodo con dos campos.

Una lista enlazada se muestra en la Figura 12.2.

5 4 1 7 18 19 9 7 45 …

(a)

(b)

LISTA

5 4 1 7 18 19 9 7 45

Figura 12.2. (a) array representado por una lista; (b) lista enlazada representada por una lista de enteros.

Los componentes de un nodo se llaman campos. Un nodo tiene al menos un campo dato o valor y un enlace (indicador o puntero) con el siguiente nodo. El campo enlace apunta (proporciona la dirección o referencia de) al siguiente nodo de la lista. El último nodo de la lista enlazada, por convenio, se suele representar por un enlace con la palabra

reservada nil (nulo), una barra inclinada (/) y, en ocasiones, el símbolo eléctrico de tierra o masa (Figura 12.3).

1

Las listas enlazadas se conocen también en Latinoamérica con el término «ligadas» y «encadenadas». El término en inglés es linked list.

P:30

434 Fundamentos de programación

4 4 nil 4

Figura 12.3. Representación del último nodo de una lista.

La implementación de una lista enlazada depende del lenguaje. C, C++, Pascal, PL/I, Ada y Modula-2 utilizan

simplemente como enlace una variable puntero, o puntero (apuntador). Java no dispone de punteros, por consiguiente, resuelve el problema de forma diferente y almacena en el enlace la referencia al siguiente objeto nodo. Los

lenguajes como FORTRAN y COBOL no disponen de este tipo de datos y se debe simular con una variable entera

que actúa como indicador o cursor. En nuestro libro utilizaremos a partir de ahora el término puntero (apuntador)

para describir el enlace entre dos elementos o nodos de una lista enlazada.

Un puntero (apuntador) es una variable cuyo valor es la dirección o posición de otra variable.

En las listas enlazadas no es necesario que los elementos de la lista sean almacenados en posiciones físicas adyacentes, ya que el puntero indica dónde se encuentra el siguiente elemento de la lista, tal como se indica en la Figura 12.4.

NULO

INFO SIG

INFO SIG

INFO SIG

INFO SIG

PRIMERO

Figura 12.4. Elementos no adyacentes de una lista enlazada.

Por consiguiente, la inserción y borrado no exigen desplazamiento como en el caso de las listas contiguas.

Para eliminar el 45.º elemento ('INÉS') de una lista lineal con 2.500 elementos [Figura 12.5 (a)] sólo es necesario cambiar el puntero en el elemento anterior, 44.º, y que apunte ahora al elemento 46.º [Figura 12.5 (b)]. Para

insertar un nuevo elemento ('HIGINIO') después del 43.º ('GONZALO') elemento, es necesario cambiar el puntero

del elemento 43.º y hacer que el nuevo elemento apunte al elemento 44.º [Figura 12.5 (c)].

GONZALO HORACIO INÉS IVÁN JUAN ZAYA NULO

(a)

GONZALO HORACIO INÉS IVÁN JUAN ZAYA NULO

(b)

(c)

HIGINI O

GONZALO HORACIO IVÁN JUAN ZAYA NULO

Figura 12.5. Inserción y borrado de elementos.

P:31

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 435

Una lista enlazada sin ningún elemento se llama lista vacía. Su puntero inicial o de cabecera tiene el valor nulo

(nil).

Una lista enlazada se define por:

• El tipo de sus elementos: campo de información (datos) y campo enlace (puntero o apuntador).

• Un puntero de cabecera que permite acceder al primer elemento de la lista.

• Un medio para detectar el último elemento de la lista: puntero nulo (nil).

EJEMPLO 12.3

El director de un hotel desea registrar el nombre de cada cliente a medida de su llegada al hotel, junto con el número de habitación que ocupa —el antiguo libro de entradas—. También desea disponer en cualquier momento de

una lista de sus clientes por orden alfabético.

Ya que no es posible registrar los clientes alfabética y cronológicamente en la misma lista, se necesita o bien

listas alfabéticas independientes o bien añadir punteros a la lista existente, con lo que sólo se utilizará una única lista. El método manual en el libro requería muchos cruces y reescrituras; sin embargo, una computadora mediante un

algoritmo adecuado lo realizará fácilmente.

Por cada nodo de la lista el campo de información o datos tiene dos partes: nombre del cliente y número de habitación. Si x es un puntero a uno de estos nodos, l[x].nombre y l[x].habitación representarán las dos partes del

campo información.

El listado alfabético se consigue siguiendo el orden de los punteros de la lista (campo puntero). Se utiliza una

variable CABECERA(S) para apuntar al primer cliente.

CABECERA ← 3

Así, CABECERA(S) es 3, ya que el primer cliente, Antolín, ocupa el lugar 3. A su vez, el puntero asociado al

nodo ocupado por Antolín contiene el valor 10, que es el segundo nombre de los clientes en orden alfabético y éste

tiene como campo puntero el valor 7, y así sucesivamente. El campo puntero del último cliente, Tomás, contiene el

puntero nulo indicado por un 0 o bien una Z.

Registro Nombre Habitación Puntero

S(=3) 1 Tomás 324 z (final)

2 Cazorla 28 8

3 Antolín 95 10

4 Pérez 462 6

5 López 260 12

6 Sánchez 220 1

7 Bautista 115 2

8 García 105 9

9 Jiménez 173 5

10 Apolinar 341 7

11 Martín 205 4

12 Luzárraga 420 11

.

.

.

.

.

.

.

.

.

.

.

.

Figura 12.6. Lista enlazada de clientes de un hotel.

P:32

436 Fundamentos de programación

12.4. PROCESAMIENTO DE LISTAS ENLAZADAS

Para procesar una lista enlazada se necesitan las siguientes informaciones:

• Primer nodo (cabecera de la lista).

• El tipo de sus elementos.

Las operaciones que normalmente se ejecutan con listas incluyen:

1. Recuperar información de un nodo específico (acceso a un elemento).

2. Encontrar el nodo que contiene una información específica (localizar la posición de un elemento dado).

3. Insertar un nuevo nodo en un lugar específico de la lista.

4. Insertar un nuevo nodo en relación a una información particular.

5. Borrar (eliminar) un nodo existente que contiene información específica.

12.4.1. Implementación de listas enlazadas con punteros

Como ya hemos visto, la representación gráfica de un puntero consiste en una flecha que sale del puntero y llega a

la variable dinámica apufintada.

Para declarar una variable de tipo puntero:

tipo

puntero_a <tipo_dato>: punt

var

punt : p, q

El <tipo_dato> podrá ser simple o estructurado.

Operaciones con punteros:

Inicialización

p ← nulo A nulo para indicar que no apunta a ninguna variable.

Comparación

p = q Con los operadores = o <>.

p

q

p→ q→

Asignación

p ← q Implica hacer que el puntero p apunte a donde apunta q.

P:33

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 437

Creación de variables dinámicas

Reservar(p) Reservar espacio en memoria para la variable dinámica.

Eliminación de variables dinámicas

Liberar(p) Liberar el espacio en memoria ocupado por la variable dinámica.

Variables dinámicas

Variable simple o estructura de datos sin nombre y creada en tiempo de ejecución.

p p →

Para acceder a una variable dinámica apuntada, como no tiene nombre se escribe p →

Las variables p → podrán intervenir en toda operación o expresión de las permitidas para una variable estática

de su mismo tipo.

Nodo

Las estructuras dinámicas de datos están formadas por nodos.

Un nodo es una variable dinámica constituida por al menos dos campos:

• el campo dato o valor (elemento);

• el campo enlace, en este caso de tipo puntero (sig).

elemento sig

tipo

registro: nodo

//elemento es el campo que contiene la información

<tipo_elemento>: elemento

//punt apunta al siguiente elemento de la estructura

punt : sig

{según la estructura de que se trate podrá haber uno o varios

campos de tipo punt}

... : ...

fin_registro

Creación de la lista

La creación de la lista conlleva la inicialización a nulo del puntero (inic), que apunta al primer elemento de la lista.

tipo

puntero_a nodo: punt

registro: tipo_elemento

... : ...

fin_registro

registro: nodo

P:34

438 Fundamentos de programación

tipo_elemento : elemento

punt : sig

fin_registro

var punt : inic, posic, anterior

tipo_elemento : elemento

logico : encontrado

inicio

inicializar(inic)

...

fin

procedimiento inicializar(S punt: inic)

inicio

inic ← nulo

fin_procedimiento

inic

nulo

Inserción de un elemento

La inserción tiene dos casos particulares:

• Insertar el nuevo nodo en el frente, principio de la lista.

• Insertar el nuevo nodo en cualquier otro lugar de la lista.

El procedimiento insertar inserta un nuevo elemento a continuación de anterior, si anterior fuera nulo

significa que ha de insertarse al comienzo de la lista.

1.º

anterior →

elemento

xxxx

zzzz anterior

nulo

inic

insertar(inic, anterior, elemento)

2.º

3.º

auxi→

elemento sig

yyyy

auxi

auxi

P:35

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 439

anterior

xxxx

zzzz anterior

nulo

inic

4.º

5.º

yyyy

elemento sig

auxi

auxi

1.º Situación de partida.

2.º reservar(auxi).

3.º Introducir la nueva información en auxi→.elemento.

4.º Hacer que auxi→ sig apunte a donde lo hacía anterior→.sig.

5.º Conseguir que anterior→.sig apunte a donde lo hace auxi.

procedimiento insertar( E/S punt: inic,anterior;

E tipo_elemento: elemento)

var punt: auxi

inicio

reservar(auxi)

auxi→.elemento ← elemento

si anterior = nulo entonces

auxi →.sig ← inic

inic ← auxi

si_no

auxi→.sig ← anterior→.sig

anterior→.sig ← auxi

fin_si

anterior ← auxi // Opcional

fin_procedimiento

Eliminación de un elemento de una lista enlazada

Antes de proceder a la eliminación de un elemento de la lista, deberemos comprobar que no está vacía. Para lo que

podremos recurrir a la función vacia.

lógico función vacia(E punt: inic)

inicio

devolver(inic = nulo)

fin_función

Al suprimir un elemento de una lista consideraremos dos casos particulares:

• El elemento a suprimir está al principio de la lista.

• El elemento se encuentra en cualquier otro lugar de la lista.

1.º Situación de partida.

2.º anterior →.sig apunta a donde posic →.sig.

3.º liberar(posic).

P:36

440 Fundamentos de programación

procedimiento suprimir(E/S punt: inic, anterior, posic)

inicio

si anterior = nulo entonces

inic ← posic→.sig

si_no

anterior→.sig ← posic→.sig

fin_si

liberar(posic)

anterior ← nulo // Opcional

posic ← inic // Opcional

fin_procedimiento

anterior

elemento

xxxx

zzzz

anterior

nulo

inic

yyyy

elemento sig posic

posic

1.º

Suprimir(inic, anterior, posic)

anterior →

elemento

xxxx

zzzz

anterior

nul

3.º 2.º o

inic

yyyy sig

posic

P:37

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 441

Java y C# permiten la creación de listas enlazadas vinculando objetos nodo sin el empleo de punteros, en el enlace se almacena la referencia al siguiente nodo de la lista. En estos lenguajes, aunque la implementación de una

lista enlazada resulta similar, hay que tener en cuenta que al crear un objeto nodo se reserva espacio en memoria para

él y que este espacio se libera automáticamente, a través de un proceso denominado recolección automática de basura, cuando dicho objeto (nodo) deja de estar referenciado.

Recorrido de una lista enlazada

Para recorrer la lista utilizaremos una variable de tipo puntero auxiliar.

procedimiento recorrer(E punt:inic)

var punt: posic

inicio

posic ← inic

mientras posic <> nulo hacer

proc_escribir(posic→.elemento)

posic ← posic→.sig

fin_mientras

fin_procedimiento

EJEMPLO 12.4

Cálculo del número de elementos de una lista enlazada.

procedimiento contar(E punt: primero; S entero: n)

var punt: p

inicio

n ← 0 //contador de elementos

p ← primero

mientras p <> nulo hacer

n ← n + 1

p ← p →.sig

fin_mientras

fin_procedimiento

Acceso a un elemento de una lista enlazada

La búsqueda de una información en una lista simplemente enlazada sólo puede hacerse mediante un proceso secuencial o recorrido de la lista elemento a elemento, hasta encontrar la información buscada o detectar el final de la lista.

procedimiento consultar( E punt: inic; S punt: posic,anterior;

E tipo_elemento: elemento; S lógico: encontrado)

inicio

encontrado ← falso

anterior ← nulo

posic ← inic

mientras no igual(posic→.elemento,elemento) y (posic <> nulo) hacer

{ igual es una función que compara los elementos que le pasamos como

parámetros, recurrimos a ella porque, si se tratara de registros,

compararíamos únicamente la información almacenada en un

determinado campo }

si

fin_mientras

si igual(posic→.elemento, elemento) entonces

encontrado ← verdad

P:38

442 Fundamentos de programación

si_no

encontrado ← falso

fin_si

fin_procedimiento

EJEMPLO 12.5

Encontrar el nodo de una lista que contiene la información de valor t, suponiendo que la lista almacena datos de

tipo entero.

procedimiento encontrar(E punt: primero E entero: t)

var punt : p

entero: n

inicio

n ← 0

p ← primero

mientras (p→.info <> t) y (p <> nulo) hacer

n ← n + 1

p ← p →.sig

fin_mientras

si p→.info = t entonces

escribir('Se encuentra en el nodo ',n,' de la lista')

si_no

escribir('No encontrado')

fin_si

fin_procedimiento

Considere que la información se encuentra almacenada en la lista de forma ordenada, orden creciente, y mejore

la eficacia del algoritmo anterior.

procedimiento encontrar(E punt: primero E entero: t)

var punt : p

entero: n

inicio

n ← 0

p ← primero

mientras (p →.info < t) y (p <> nulo) hacer

n ← n + 1

p ← p →.sig

fin_mientras

si p →.info = t entonces

escribir('Se encuentra en el nodo ',n,' de la lista')

si_no

escribir('No encontrado')

fin_si

fin_procedimiento

12.4.2. Implementación de listas enlazadas con arrays (arreglos)

Las listas enlazadas deberán implementarse de forma dinámica, pero si el lenguaje no lo permite, lo realizaremos a

través de arrays (o arreglos), con lo cual impondremos limitaciones en cuanto al número de elementos que podrá

contener la lista y estableceremos una ocupación en memoria constante.

Los nodos podrán almacenarse en arrays paralelos o arrays de registros.

Cuando se empleen arrays de registros, el valor (dato o información) del nodo se almacenará en un campo y el

enlace con el siguiente elemento se almacenará en otro.

Otra posible implementación, como ya se ha dicho antes, es con dos arrays: uno para los datos y otro para el enlace.

P:39

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 443

Un valor de puntero 0, o bien Z, indica el final de la lista.

ELEMENTO SIG ELEMENTO SIG

XXXXXXXXXXXXX 1 2 1 XXXXXXXXXXXXX 2

XXXXXXXXXXXXX 2 4 2 XXXXXXXXXXXXX 4

3 3

XXXXXXXXXXXXX 4 6 4 XXXXXXXXXXXXX 6

5 5

XXXXXXXXXXXXX 6 0 6 XXXXXXXXXXXXX 0

Para definir la lista se debe especificar la variable que apunta al primer nodo (cabecera), que en nuestro caso

denominaremos inic.

inic ← 1

Para insertar un nuevo elemento, que siga a m[1] y sea seguido por m[2], lo único que se hará es modificar los

punteros.

M SIG

1 XXXXXXXXXXXXX 3

2 XXXXXXXXXXXXX 4

3 XXXXXXXXXXXXX 2

4 XXXXXXXXXXXXX 6

5

6 XXXXXXXXXXXXX 0

Como el nuevo elemento se coloca en la primera posición libre deberemos tener un puntero vacío que apunte a

dicha primera posición libre.

Es decir, utilizaremos el array para almacenar dos listas, la lista de elementos y la lista de vacíos.

Es, pues, necesario, al comenzar a trabajar, crear la lista de vacíos de la forma que a continuación se expone:

• vacio apunta al primer registro libre.

• En el campo sig de cada registro se almacena información sobre el siguiente registro disponible.

• Cuando lleguemos al último registro libre, su campo sig recibirá el valor 0, para indicar que ya no quedan

más registros disponibles.

ELEMENTO SIG

1 2

2 3

vacio → 1 3 4

inic → 0 4 5

5 6

6 0

P:40

444 Fundamentos de programación

Insertar el primer elemento

1 XXXXXXXXXXXXX 0

2 3

vacio → 2 3 4

inic → 1 4 5

5 6

6 0

Al implementar una lista a través de arrays necesitaremos los procedimientos:

inicializar( ) iniciar( ) consultar( ) insertar( ) suprimir( )

reservar( ) liberar( )

y las funciones

vacia( ) llena( )

El procedimiento reservar( ) nos proporcionará la primera posición vacía para almacenar un nuevo elemento

y la eliminará de la lista de vacíos, pasando el puntero de vacíos (vacio) a la posición siguiente, vacio toma el

valor del siguiente vacio de la lista.

Liberar( ) inserta un nuevo elemento en la lista de vacíos. Se podrían adoptar otras soluciones, pero nuestro

procedimiento liberar insertará el nuevo elemento en la lista de vacíos por delante, sobrescribiendo el campo

m[posic].sig para que apunte al que antes era el primer vacio. El puntero de inicio de los vacíos (vacio) lo

cambiará al nuevo elemento.

Creación de la lista

Consideraremos el array como si fuera la memoria del ordenador y guardaremos en él dos listas: la lista de elementos y la de vacíos.

El primer elemento de la lista de elementos está apuntado por inic y, por vacio, el primero de la lista de vacíos:

const

max = <expresión>

tipo

registro: tipo_elemento

... : ...

... : ...

fin_registro

registro: tipo_nodo

tipo_elemento : elemento

entero : sig

{ actúa como puntero, almacenando la posición donde se

encuentra el siguiente elemento de una lista }

fin_registro

array[1..max] de tipo_nodo: arr

var

entero : inic,

posic,

anterior,

vacio

arr : m

// m representa la memoria de nuestra computadora

P:41

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 445

tipo_elemento : elemento

logico : encontrado

inicio

iniciar(m, vacio)

inicializar(inic)

...

fin

Al comenzar:

procedimiento inicializar(S entero: inic) //lista de elementos

inicio

inic ← 0

fin_procedimiento

elemento sig

vacio ← 1 indica que el primer registro libre es m[1] 1 2

2 3

3 4

4 5

inic ← 0 inic señala que no hay elementos en la lista 5 6

6 0

procedimiento iniciar(S arr: m; S entero: vacio) //lista de vacíos

var

entero: i

inicio

vacio ← 1

desde i ← 1 hasta max-1 hacer

m[i].sig ← i+1

fin_desde

m[max].sig ← 0

//Como ya no hay más posiciones libres a las que apuntar, recibe un 0

fin_procedimiento

Al trabajar de esta manera conseguiremos que la inserción o borrado de un determinado elemento, n-ésimo, de

la lista no requiera el desplazamiento de otros.

Inserción de un elemento

Al actualizar una lista se pueden presentar dos casos particulares:

• Desbordamiento (overflow).

• Subdesbordamiento o desbordamiento negativo (underflow).

El desbordamiento se produce cuando la lista está llena y la lista de espacio disponible está vacía.

El subdesbordamiento se produce cuando se tiene una lista vacía y se desea borrar un elemento de la misma.

Luego, para poder insertar un nuevo elemento en una lista enlazada, es necesario comprobar que se dispone de

espacio libre para ello. Al insertar un nuevo elemento en la lista deberemos recurrir al procedimiento reservar(...)

que nos proporcionará, a través de auxi, la primera posición vacía para almacenar en ella el nuevo elemento, eliminando dicha posición de la lista de vacíos.

P:42

446 Fundamentos de programación

Por ejemplo, al insertar el primer elemento:

elemento Sig

1 XXXXXXXXXXXXX 2 0

2 3 0

auxi ← 1 3 4

vacio ← 2 4 5

5 6

6 0

• vacio señala que la primera posición libre es la 1 auxi ← 1

• El campo sig del registro m[vacio] proporciona la siguiente posición vacía y reservar hará que vacio

apunte a esta nueva posición vacio ← 2

Al insertar un segundo elemento:

• como vacio tiene el valor 2 auxi ← 2

• vacio ← m[2].sig, es decir vacio ← 3

elemento Sig

1 XXXXXXXXXXXXX 2 0

2 XXXXXXXXXXXXX 3

auxi ← 2 3 4

vacio ← 3 4 5

5 6

6 0

procedimiento reservar(S entero: auxi; E arr: m; E/S entero: vacio)

inicio

si vacio = 0 entonces

// Memoria agotada

auxi ← 0

si_no

auxi ← vacio

vacio ← m[vacio].sig

fin_si

fin_procedimiento

El procedimiento insertar colocará un nuevo elemento a continuación de anterior, si anterior fuera 0 significa que

ha de insertarse al comienzo de la lista.

procedimiento insertar( E/S entero: inic, anterior;

E tipo_elemento: elemento;

E/S arr: m ; E/S entero: vacio)

var

entero: auxi

inicio

reservar(auxi,m,vacio)

si auxi = 0 entonces OVERFLOW

m[auxi].elemento ← elemento

P:43

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 447

si anterior = 0 entonces

m[auxi].sig ← inic

inic ← auxi

si_no

m[auxi].sig ← m[anterior].sig

m[anterior].sig ← auxi

fin_si

anterior ← auxi // Opcional

{ Prepara anterior para que, si no especificamos otra cosa, la

siguiente inserción se realice a continuación de la actual}

fin_procedimiento

Consideremos la siguiente situación y analicemos el comportamiento que en ella tendrían los procedimientos

reservar e insertar: Se desea insertar un nuevo elemento en la lista a continuación del primero y la situación

actual, tras sucesivas inserciones y eliminaciones, es como se muestra a continuación:

1 XXXXXXXXXXXXX 2

2 XXXXXXXXXXXXX 4

vacio ← 3 3 5

inic ← 1 4 XXXXXXXXXXXXX 6

5 0

6 XXXXXXXXXXXXX 0

El nuevo elemento se colocará en el array en la primera posición libre y lo único que se hará es modificar los

punteros.

reservar(...) proporciona la primera posición libre

1 XXXXXXXXXXXXX 3

2 XXXXXXXXXXXXX 4

auxi ← 3 3 nuevo_elemento 2

4 XXXXXXXXXXXXX 6

vacio ← 5 5 0

6 XXXXXXXXXXXXX 0

m[3].elemento ← nuevo_elemento

como queremos insertar el nuevo elemento a continuación del primero de la lista, su anterior será el apuntado por inic

anterior ← 1

m[3].sig ← 2

m[1].sig ← 3

Eliminación de un elemento

Para eliminar un elemento de la lista deberemos recurrir al procedimiento suprimir(...), que, a su vez, llamará

al procedimiento liberar(...) para que inserte el elemento eliminado en la lista de vacíos.

Supongamos que se trata de eliminar el elemento marcado con ************* cuya posición es 3

posic ← 3

el elemento anterior al 3 ocupa en el array la posición 2

anterior ← 2

P:44

448 Fundamentos de programación

y el primer vacío está en 5. Siendo el aspecto actual de la lista el siguiente:

elemento sig

inic ← 1 1 XXXXXXXXXXXXX 2

anterior ← 2 2 XXXXXXXXXXXXX 3

posic ← 3 3 ************* 4

4 XXXXXXXXXXXXX 0

vacio ← 5 5 6

6 0

Al suprimir el elemento 3 la lista quedaría:

m[2].sig ← 4

mediante el procedimiento liberar(...) incluimos el nuevo vacio en la lista de vacíos

m[3].sig ← 5 vacio ← 3

como el que se suprime no es el primer elemento de la lista, el valor de inic no varía

inic ← 1

elemento sig

1 XXXXXXXXXXXXX 2

2 XXXXXXXXXXXXX 4

3 ************* 5

4 XXXXXXXXXXXXX 0

5 6

6 0

procedimiento liberar(E entero: posic; E/S arr: m; E/S entero: vacio)

inicio

m[posic].sig ← vacio

vacio ← posic

fin_procedimiento

procedimiento suprimir( E/S entero: inic, anterior, posic; E/S arr: m;

E/S entero: vacio)

inicio

si anterior = 0 entonces

inic ← m[posic].sig

si_no

m[anterior].sig ← m[posic].sig

fin_si

liberar(posic, m, vacio)

anterior ← 0 // Opcional

posic ← inic // Opcional

{ Las dos últimas instrucciones preparan los punteros para que,

si no se especifica otra cosa, la próxima eliminación se realice

por el principio de la lista }

fin_procedimiento

P:45

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 449

Recorrido de una lista

El recorrido de la lista se realizará siguiendo los punteros a partir de su primer elemento, el señalado por inic.

El procedimiento recorrer(...) que se implementa a continuación, al recorrer la lista va mostrando por pantalla los diferentes elementos que la componen.

procedimiento recorrer(E entero: inic)

var entero: posic

inicio

posic ← inic

mientras posic <> 0 hacer

{ Recurrimos a un procedimiento, proc_escribir(...),

para presentar por pantalla los campos del registro

pasado como parámetro }

proc_escribir(m[posic].elemento)

posic ← m[posic].sig

fin_mientras

fin_procedimiento

Búsqueda de un determinado elemento en una lista

El procedimiento consultar informará sobre si un determinado elemento se encuentra o no en la lista, la posición

que ocupa dicho elemento en el array y la que ocupa el elemento anterior. Si la información se encontrara colocada

en la lista de forma ordenada y creciente por el campo de búsqueda el procedimiento de consulta podría ser el siguiente:

procedimiento consultar( E entero: inic; S entero: posic, anterior;

E tipo_elemento: elemento; S lógico: encontrado;

E arr: m)

inicio

anterior ← 0

posic ← inic

{ Las funciones menor() e igual()comparan los registros por

un determinado campo}

mientras menor(m[posic].elemento,elemento) y (posic <> 0) hacer

anterior ← posic

posic ← m[posic].sig

fin_mientras

si(posic = 0) entonces

encontrado ← falso

si igual(m[posic].elemento,elemento) entonces

encontrado ← verdad

fin_procedimiento

Funciones

Cuando implementamos una lista enlazada utilizando arrays, necesitamos las siguientes funciones:

logico función vacia(E entero: inic)

inicio

devolver(inic = 0)

fin_función

lógico función llena(E entero: vacio)

inicio

devolver(vacio = 0)

fin_función

P:46

450 Fundamentos de programación

12.5. LISTAS CIRCULARES

Las listas simplemente enlazadas no permiten a partir de un elemento acceder directamente a cualquiera de los elementos que le preceden. En lugar de almacenar un puntero NULO en el campo SIG del último elemento de la lista, se

hace que el último elemento apunte al primero o principio de la lista. Este tipo de estructura se llama lista enlazada

circular o simplemente lista circular (en algunos textos se les denomina listas en anillo).

PRIMERO INFO SIG INFO SIG INFO SIG INFO SIG

Las listas circulares presentan las siguientes ventajas respecto de las listas enlazadas simples:

• Cada nodo de una lista circular es accesible desde cualquier otro nodo de ella. Es decir, dado un nodo se puede recorrer toda la lista completa. En una lista enlazada de forma simple sólo es posible recorrerla por completo si se parte de su primer nodo.

• Las operaciones de concatenación y división de listas son más eficaces con listas circulares.

Los inconvenientes, por el contrario, son:

• Se pueden producir lazos o bucles infinitos. Una forma de evitar estos bucles infinitos es disponer de un nodo

especial que se encuentre permanentemente asociado a la existencia de la lista circular. Este nodo se denomina

cabecera de la lista.

CABECERA SIG INFO SIG INFO SIG INFO SIG

Figura 12.7. Nodo cabecera de la lista.

El nodo cabecera puede diferenciarse de los otros nodos en una de las dos formas siguientes:

• Puede tener un valor especial en su campo INFO que no es válido como datos de otros elementos.

• Puede tener un indicador o bandera (flag) que señale cuando es nodo cabecera.

El campo de la información del nodo cabecera no se utiliza, lo que se señala con el sombreado de dicho campo.

Una lista enlazada circularmente vacía se representa como se muestra en la Figura 12.8.

CABECERA

Figura 12.8. Lista circular vacía.

12.6. LISTAS DOBLEMENTE ENLAZADAS

En las listas lineales estudiadas anteriormente el recorrido de ellas sólo podía hacerse en un único sentido: de izquierda a derecha (principio a final). En numerosas ocasiones se necesita recorrer las listas en ambas direcciones.

Las listas que pueden recorrerse en ambas direcciones se denominan listas doblemente enlazadas. En estas listas

cada nodo consta del campo INFO de datos y dos campos de enlace o punteros: ANTERIOR(ANT) y SIGUIENTE(SIG)

P:47

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 451

que apuntan hacia adelante y hacia atrás (Fig. 12.9). Como cada elemento tiene dos punteros, una lista doblemente enlazada ocupa más espacio en memoria que una lista simplemente enlazada para una misma cantidad de información.

La lista necesita dos punteros CABECERA y FIN2

que apuntan hacia el primero y último nodo.

La variable CABECERA y el puntero SIG permiten recorrer la lista en el sentido normal y la variable FIN y el

puntero ANT permiten recorrerla en sentido inverso.

NODO

ANT INFO SIG

(a

(b

IZQUIERDA DERECHA

Figura 12.9. Lista doblemente enlazada.

x x

CABECERA Campo INFO del nodo n FIN

Campo ANT del nodo n

Campo puntero SIG del nodo n

Nodo n

Figura 12.10. Lista doble.

Como se ve en la Figura 12.11, una propiedad fundamental de las listas doblemente enlazadas es que para cualquier puntero P de la lista:

nodo [nodo[p].sig].ant = p

nodo [nodo[p].ant].sig = p

p → ant

ant ant ant

p p → → sig

P

sig sig sig

Figura 12.11.

12.6.1. Inserción

La inserción de un nodo a la derecha de un nodo especificado, cuya dirección está dada por la variable M, puede

presentar varios casos:

1. La lista está vacía; se indica mediante M = NULO y CABECERA y FIN son también NULO. Una inserción indica

que CABECERA se debe fijar con la dirección del nuevo nodo y los campos ANT y SIG también se establecen

en NULO.

2. Insertar dentro de la lista: existe un elemento anterior y otro posterior de nuevo nodo.

3. Insertar a la derecha del nodo del fin de la lista. Se requiere que el apuntador FIN sea modificado.

2

Se adoptan estos términos a efectos de normalización, pero el lector puede utilizar IZQUIERDA y DERECHA.

P:48

452 Fundamentos de programación

CABECERA FIN

antes

M

CABECERA FIN

después x

Figura 12.12. Inserción en un lista doblemente enlazada.

CABECERA

antes

CABECERA

FIN

FIN

M

M

después

Figura 12.13. Inserción en el extremo derecho de una lista doblemente enlazada.

12.6.2. Eliminación

La operación de eliminación es directa. Si la lista tiene un simple nodo, entonces los punteros de los extremos izquierdo y derecho asociados a la lista se deben fijar en NULO. Si el nodo del extremo derecho de la lista es el señalado para la eliminación, la variable FIN debe modificarse para señalar el predecesor del nodo que se va a borrar de

la lista. Si el nodo del extremo izquierdo de la lista es el que se desea borrar, la variable CABECERA debe modificarse para señalar el elemento siguiente.

La eliminación se puede realizar dentro de la lista (Figura 12.14).

CABECERA FIN

antes

CABECERA FIN

ANT (X) X

X

después

Figura 12.14. Eliminación de un nodo X en una lista doblemente enlazada.

12.7. PILAS

Una pila (stack) es un tipo especial de lista lineal en la que la inserción y borrado de nuevos elementos se realiza

sólo por un extremo que se denomina cima o tope (top).

La pila es una estructura con numerosas analogías en la vida real: una pila de platos, una pila de monedas, una

pila de cajas de zapatos, una pila de camisas, una pila de bandejas, etc.

P:49

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 453

Una pila

de bandejas

Una pila

de monedas

Una pila

de cajas

de zapatos

Una pila de camisas

Figura 12.15. Ejemplos de tipos de pilas.

Dado que las operaciones de insertar y eliminar se realizan por un solo extremo (el superior), los elementos sólo

pueden eliminarse en orden inverso al que se insertan en la pila. El último elemento que se pone en la pila es el

primero que se puede sacar; por ello, a estas estructuras se les conoce por el nombre de LIFO (last-in, first-out, último en entrar, primero en salir).

Las operaciones más usuales asociadas a las pilas son:

"push" Meter, poner o apilar: operación de insertar un elemento en la pila.

"pop" Sacar, quitar o desapilar: operación de eliminar un elemento de la pila.

Las pilas se pueden representar en cualquiera de las tres formas de la Figura 12.16.

FFF

EEE

DDD

CCC

BBB

AAA

Cima

(a)

AAA

BBB

CCC

DDD

EEE

FFF

Cima

(b)

AAA BBB CCC DDD EEE FFF

1 2 3 4 5 6 7 8 9 ... N – 1 N

Cima (c)

Figura 12.16. Representación de las pilas.

Para representar una pila St, se debe definir un vector con un determinado tamaño (longitud máxima):

var array [1..n] de <tipo_dato> : St

Se considerará un elemento entero P como el puntero de la pila (stack pointer). P es el subíndice del array correspondiente al elemento cima de la pila (esto es, el que ocupa la última posición). Si la pila está vacía, P = 0.

(Véase la figura de la página siguiente.)

En principio, la pila está vacía y el puntero de la pila o CIMA está a cero. Al meter un elemento en la pila, se

incrementa el puntero en una unidad. Al sacar un elemento de la pila se decrementa en una unidad el puntero.

Al manipular una pila se deben realizar algunas comprobaciones. En una pila vacía no se pueden sacar datos

(P = 0). Si la pila se implementa con un array de tamaño fijo, se puede llenar cuando P = n (n, longitud total de la

pila) y el intento de introducir más elementos en la pila producirá un desbordamiento de la pila.

P:50

454 Fundamentos de programación

}

1 2 4

...

n – 1 n

...

p – 1 p n – 2

Cima

(puntero de la pila)

Longitud

máxima de la pila

Parte no utilizada actualmente de la pila

3

Idealmente una pila puede contener un número ilimitado de elementos y no producir nunca desbordamiento. En

la práctica, sin embargo, el espacio de almacenamiento disponible es finito. La codificación de una pila requiere un

cierto equilibrio, ya que si la longitud máxima de la pila es demasiado grande se gasta mucha memoria, mientras que

un valor pequeño de la longitud máxima producirá desbordamientos frecuentes.

Para trabajar fácilmente con pilas es conveniente diseñar subprogramas de poner (push) y quitar (pop) elementos.

También es necesario con frecuencia comprobar si la pila está vacía; esto puede conseguirse con una variable o función booleana VACIA, de modo que cuando su valor sea verdadero la pila está vacía y falso en caso contrario.

P = CIMA Puntero de la pila.

VACIA Función booleana «pila vacía».

PUSH Subprograma para añadir, poner o insertar elementos.

POP Subprograma para eliminar o quitar elementos.

LONGMAX Longitud máxima de la pila.

X Elemento a añadir/quitar de la pila.

Implementación con punteros

Si el lenguaje tiene punteros, deberemos implementar las pilas con punteros.

Para la manipulación de una pila mediante punteros es preciso diseñar los siguientes procedimientos y/o funciones: inicializar o crear, apilar o meter, desapilar o sacar, consultarCima y Vacia.

algoritmo pilas_con_punteros

tipo

puntero_a nodo: punt

registro : tipo_elemento

.... : ....

.... : ....

fin_registro

registro : nodo

tipo_elemento : elemento

punt : cima

fin_registro

var

punt : cima

elemento : tipo_elemento

inicio

inicializar(cima)

...

fin

procedimiento inicializar(S punt: cima)

inicio

cima ← nulo

fin_procedimiento

lógico función vacia(E punt: cima)

inicio

devolver (cima = nulo)

fin_función

P:51

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 455

procedimiento consultarCima( E punt: cima;

S tipo_elemento: elemento)

inicio

si no vacia (cima) entonces

elemento ← cima→.elemento

fin_si

fin_procedimiento

Los elementos se incorporan siempre por un extremo, cima.

meter(cima, elemento)

xxx

nulo

nulo

yyy

cima

cima

cima →

elemento cima

cima →

elemento cima

auxi

auxi

zzz

1.º

2.º

3.º

nulo

cima

cima →

elemento cima

6.º

xxx

yyy

zzz

xxx

yyy

auxi

5.º

4.º

zzz

1.º cima apunta al último elemento de la pila.

2.º reservar(auxi).

3.º Introducimos la información en auxi→.elemento.

4.º Hacemos que auxi→.cima apunte a donde cima.

5.º Cambiamos cima para que apunte donde auxi.

6.º La pila tiene un elemento más.

P:52

456 Fundamentos de programación

procedimiento meter(E/S punt: cima; E tipo_elemento: elemento)

var

punt: auxi

inicio

reservar(auxi)

auxi→.elemento ← elemento

auxi→.cima ← cima

cima ← auxi

fin_procedimiento

Los elementos se recuperan en orden inverso a como fueron introducidos

Sacar(cima, elemento)

nulo

xxx

elemento cima

cima → cima →

yyy

auxi

1.º

2.º

zzz

cima

3.º

cima →

elemento cima

xxx

yyy

4.º

5.º

cima

auxi

zzz

cima →

elemento cima

1.º cima apunta al último elemento de la pila

2.º Hacemos que auxi apunte adonde apuntaba cima

3.º Y que cima pase a apuntar adonde cima→.cima

4.º liberar(auxi)

5.º La pila tiene un elemento menos

procedimiento sacar(E/S punt:cima; S tipo_elemento: elemento)

var

punt: auxi

inicio

si no vacia (cima) entonces

auxi ← cima

elemento ← cima →.elemento

cima ← cima →.cima

P:53

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 457

liberar(auxi)

{liberar es un procedimiento para la eliminación de

variables dinámicas}

fin_si

fin_procedimiento

Implementación con arrays

Necesitaremos un array y una variable numérica cima que apunte al último elemento colocado en la pila.

La inserción o extracción de un elemento se realizará siempre por la parte superior.

Su implementación mediante arrays limita el máximo número de elementos que la pila puede contener y origina

la necesidad de una función más.

Llena(...) de resultado lógico

cima ←0

Inicializar(cima)

cima 1

Meter(cima,p,elemento)

cima ←2

xxxxxxxx

xxxxxxxx

xxxxxxxx cima ←1 xxxxxxxx

Sacar(cima,p,elemento)

Max

...

...

3

2

1

xxxxxxxx

const Max = <expresión>

tipo

registro: tipo_elemento

... : ...

... : ...

fin_registro

array[1..Max] de tipo_elemento: arr

var

entero : cima

arr : p

tipo_elemento : elemento

inicio

inicializar(cima)

...

fin

procedimiento inicializar(S entero: cima)

inicio

cima ← 0

fin_procedimiento

logico función vacia(E entero: cima)

inicio

si cima = 0 entonces

P:54

458 Fundamentos de programación

devolver (verdad)

si_no

devolver (falso)

fin_si

fin_función

lógico función llena(E entero: cima)

inicio

si cima = Max entonces

devolver (verdad)

si_no

devolver (falso)

fin_si

fin_función

procedimiento consultarCima( E entero: cima; E arr:p;

S tipo_elemento: elemento)

inicio

si no vacia (cima) entonces

elemento ← p[cima]

fin_si

fin_procedimiento

procedimiento meter( E/S entero: cima; E/S arr: p;

E tipo_elemento: elemento)

inicio

si no llena (cima) entonces

cima ← cima + 1

p[cima] ← elemento

fin_si

fin_procedimiento

procedimiento sacar( E/S entero: cima; E arr: p;

S tipo_elemento: elemento)

inicio

si no vacia (cima) entonces

elemento ← p[cima]

cima ← cima - 1

fin_si

fin_procedimiento

12.7.1. Aplicaciones de las pilas

Las pilas son utilizadas ampliamente para solucionar una amplia variedad de problemas. Se utilizan en compiladores,

sistemas operativos y en programas de aplicación. Veamos algunas de las aplicaciones más interesantes.

Llamadas a subprogramas

Cuando dentro de un programa se realizan llamadas a subprogramas, el programa principal debe recordar el lugar

donde se hizo la llamada, de modo que pueda retornar allí cuando el subprograma se haya terminado de ejecutar.

Supongamos que tenemos tres subprogramas llamados A, B y C, y supongamos también que A invoca a B y B

invoca a C. Entonces B no terminará su trabajo hasta que C haya terminado y devuelto su control a B. De modo

similar, A es el primero que arranca su ejecución, pero es el último que la termina, tras la terminación y retorno

de B.

Esta operación se consigue disponiendo las direcciones de retorno en una pila.

P:55

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 459

Programa principal

X

Y

Z Subprograma A

Subprograma A

fin _ subprograma A

fin _ subprograma B

fin _ subprograma C

Subprograma C

Subprograma B

Subprograma B

Subprograma C

Puntero de pila

Pila de direcciones

de retorno

Programa principal

llamar _ a A

llamar _ a B

direccion X

direccion Y

direccion Z

llamar a C

fin

Cuando un subprograma termina, debe retornar a la dirección siguiente a la instrucción que le llamó (llamar_a).

Cada vez que se invoca un subprograma, la dirección siguiente (X, Y o Z) se introduce en la pila. El vaciado de la

pila se realizará por los sucesivos retornos, decrementándose el puntero de pila que queda libre apuntando a la siguiente dirección de retorno.

←Puntero de pila (P)

X

Y

Z

X

Y

X ← P

Retorno a

subprograma B

Retorno a

subprograma A

Retorno a

programa principal

← P

← P

EJEMPLO 12.6

Se desea leer un texto y separar los caracteres letras, dígitos y restantes caracteres para ser utilizados posteriormente.

Utilizaremos tres pilas (LETRAS, DIGITOS, OTROSCAR) para contener los diferentes tipos de caracteres. El

proceso consiste en leer carácter a carácter, comprobar el tipo de carácter y según el resultado introducirlo en su pila

respectiva.

algoritmo lecturacaracter

const Max = <valor>

tipo

array [1..Max] de carácter:pila

var

entero : cima1, cima2, cima3

P:56

460 Fundamentos de programación

pila : pilaletras, piladigitos, pilaotroscaracteres

carácter : elemento

inicio

crear (cima1)

crear (cima2)

crear (cima3)

elemento ← leercar

mientras (codigo(elemento)<> 26) y no llena(cima1) y no

llena(cima2)y no llena(cima3) hacer

{saldremos del bucle en cuanto se llene alguna de las pilas o

pulsemos ^Z}

si (elemento >= 'A') y (elemento <= 'Z') o (elemento >= 'a') y

(elemento >= 'z')entonces

meter (cima1, pilaletras, elemento)

si_no

si (elemento>= '0') y (elemento<='9') entonces

meter (cima2, piladigitos, elemento)

si_no

meter (cima3, pilaotroscaracteres, elemento)

fin_si

fin_si

elemento ← leercar

fin_mientras

fin

procedimiento crear (S entero: cima)

inicio

cima ← 0

fin_procedimiento

lógico función llena (E entero: cima)

inicio

devolver (cima = Max)

fin_función

procedimiento meter(E/S entero: cima; E/S tipo_elemento: elemento)

inicio

cima ← cima+1

p [cima] ← elemento

fin_procedimiento

12.8. COLAS

Las colas son otro tipo de estructura lineal de datos similar a las pilas, diferenciándose de ellas en el modo de insertar/eliminar elementos.

Una cola (queue) es una estructura lineal de datos

var array [1..n] de <tipo_dato> : C

en la que las eliminaciones se realizan al principio de la lista, frente (front), y las inserciones se realizan en el otro

extremo, final (rear). En las colas el elemento que entró el primero sale también el primero; por ello se conoce como

listas FIFO (first-in, first-out, “primero en entrar, primero en salir”). Así, pues, la diferencia con las pilas reside en

el modo de entrada/salida de datos; en las colas las inserciones se realizan al final de la lista, no al principio. Por ello

las colas se usan para almacenar datos que necesitan ser procesados según el orden de llegada.

P:57

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 461

Eliminación Inserción

Frente Final

x[1] x[2] x[3] x[n – 1] x[n]

.....

En la vida real se tienen ejemplos numerosos de colas: la cola de un autobús, la cola de un cine, una caravana de

coches en una calle, etc. En todas ellas el primer elemento (pasajero, coche, etc.) que llega es el primero que sale.

En informática existen también numerosas aplicaciones de las colas. Por ejemplo, en un sistema de tiempo compartido suele haber un procesador central y una serie de periféricos compartidos: discos, impresoras, etc. Los recursos se comparten por los diferentes usuarios y se utiliza una cola para almacenar los programas o peticiones de los

diferentes usuarios que esperan su turno de ejecución. El procesador central atiende —normalmente— por riguroso

orden de llamada del usuario; por tanto, todas las llamadas se almacenan en una cola. Existe otra aplicación muy

utilizada que se denomina cola de prioridades; en ella el procesador central no atiende por riguroso orden de llamada, aquí el procesador atiende por prioridades asignadas por el sistema o bien por el usuario, y sólo dentro de las

peticiones de igual prioridad se producirá una cola.

12.8.1. Representación de las colas

Las colas se pueden representar por listas enlazadas o por arrays.

Se necesitan dos punteros: frente(f) y final(r), y la lista o array de n elementos (LONGMAX).

1 2 f–1 f f+1 r–1 r r+1 n–2 n–1 n

Parte no utilizada de la lista Cola Parte no utilizada de la lista

si la cola está vacía frentre = nulo o bien f ← 0

eliminar elementos frentre ← frentre + 1 o bien f ← f + 1

añadir elementos final ← final + 1 o bien f ← r + 1

La Figura 12.17 muestra la representación de una cola mediante un array o mediante una lista enlazada.

Las operaciones que se pueden realizar con una cola son:

(b) final

[1] [2] [3] [4]

100 264 119 48

(a)

100 264 119 48

frente

frente

final

Figura 12.17. Representación de una cola: (a) mediante una lista enlazada; (b) mediante un array.

P:58

462 Fundamentos de programación

• Acceder al primer elemento de la cola.

• Añadir un elemento al final de cola.

• Eliminar el primer elemento de la cola.

• Vaciar la cola.

• Verificar el estado de la cola: vacía o llena.

Implementación con estructuras dinámicas

Aunque, como posteriormente veremos, las colas se pueden simular mediante un array y dos variables numéricas

(frente, final), deberemos, si el lenguaje lo permite, implementarlas mediante punteros.

En una cola las eliminaciones se realizarán por el extremo denominado frente y las inserciones por el final.

Para la manipulación de una cola necesitaremos los subprogramas: inicializar o crear, consultarPrimero, poner o meter, quitar o sacar y vacia o colaVacia y los siguientes tipos de datos:

tipo

puntero_a nodo : punt

registro : tipo_elemento

... : ....

... : ....

fin_registro

registro : nodo

tipo_elemento : elemento

punt : sig

fin_registro

var

punt : frente, final

tipo_elemento : elemento

Cuando no hay elementos en la cola

frente ← nulo final ← nulo

de lo que deducimos

procedimiento inicializar(S punt: frente, final)

inicio

frente ← nulo

final ← nulo

fin_procedimiento

Meter(final, frente, elemento)

procedimiento meter( E/S punt: final; S punt: frente;

E tipo_elemento: elemento)

var

punt: auxi

inicio

reservar(auxi)

auxi→.elemento ← elemento

auxi→.sig ← nulo

si final = nulo entonces

frente ← auxi

si_no

final→.sig ← auxi

fin_si

final ← auxi

fin_procedimiento

Sacar(frente,final,elemento)

P:59

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 463

final →

elemento

xxx

yyy

final

auxi

zzz

5.º

sig

xxx

yyy

final

auxi

nulo

nulo

nulo

nulo

zzz

6.º

auxi →

elemento si g

xxx

frente

frente

frente

yyy

2.º

auxi

1.º

final

auxi

3.º zzz

4.º

Los pasos de la operación meter ():

1.º Situación de partida

2.º reservar (auxi)

3.º Introducir la nueva información en auxi →.elemento

4.º Hacer que auxi →.sig apunte a nulo

5.º Conseguir que final →.sig apunte a donde lo hace auxi

6.º Por último, final debe apuntar también a donde auxi

P:60

464 Fundamentos de programación

Imaginemos que la cola tiene un único elemento. Los elementos se extraen siempre por el frente.

1.º

xxxxxxxxxxxxxx

frente

final

nulo

nulo

3.º

xxxxxxxxxxxxxx

frente

final

auxi

2.º

1.º Estado inicial.

2.º Hacemos que auxi apunte donde lo hace frente.

3.º Extraemos la información de auxi →.elemento.

6.º

xxxxxxxxxxxxxx

frente

final

auxi

4.º

xxxxxxxxxxxxxx

frente

final

auxi

5.º nulo

nulo

4.º Hacemos que frente apunte a donde lo hace auxi →.sig.

5.º Como frente toma el valor nulo a final le damos nulo.

6.º Liberar(auxi).

P:61

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 465

procedimiento sacar( E/S punt: frente; S punt: final;

S tipo_elemento: elemento)

var

punt: auxi

inicio

auxi ← frente

elemento ← auxi→.elemento

frente ← frente→.sig

si frente = nulo entonces

final ← nulo

fin_si

liberar(auxi)

fin_procedimiento

Como los elementos se extraen siempre por el frente, la cola estará vacía cuando

frente = nulo

lógico función vacia(E punt: frente)

inicio

si frente = nulo entonces

devolver(verdad)

si_no

devolver(falso)

fin_si

fin_función

procedimiento consultarPrimero( E punt: frente; S tipo_elemento: elemento)

inicio

si no vacia (frente) entonces

elemento ← frente→.elemento

fin_sin

fin_procedimiento

Implementación utilizando estructuras de tipo array

Podemos representar las colas mediante arrays.

frente final Max

const

Max = <expresión> //longitud máxima

tipo

registro: tipo_elemento

... : ...

... : ...

fin_registro

array[1..Max] de tipo_elemento: arr

var

entero : frente, final

arr : c //la cola se define como un array

tipo_elemento : elemento

P:62

466 Fundamentos de programación

Cuando la cola esté vacía frente ← 0 final ← 0

procedimiento inicializar(S entero: frente,final)

inicio

frente ← 0

final ← 0

fin_procedimiento

El procedimiento para la inserción de un nuevo elemento deberá verificar, en primer lugar, que la cola no está

totalmente llena y, por consiguiente, no se producirá error de desbordamiento. La condición de desbordamiento se

produce cuando final = Max

procedimiento meter( E/S entero: final; S entero: frente;

E/S arr: c; E tipo_elemento: elemento)

inicio

si final = 0 entonces

frente ← 1

fin_si

final ← final + 1

c[final] ← elemento

fin_procedimiento

Para eliminar un elemento será preciso verificar, en primer lugar, que la cola no está vacía.

procedimiento sacar( E/S entero: frente,final; E arr: c;

S tipo_elemento: elemento)

inicio

si no vacia (frente) entonces

elemento ← c[frente]

frente ← frente + 1

si frente = final + 1 entonces

frente ← 0

final ← 0

fin_si

fin_si

fin_procedimiento

La cola estará vacía cuando frente = 0

lógico función vacia(E entero: frente)

inicio

devolver(frente = 0)

fin_función

Esta implementación tiene el inconveniente de que puede ocurrir que la variable final llegue al valor máximo

de la tabla, con lo cual no se puedan seguir añadiendo elementos a la cola, aun cuando queden posiciones libres a la

izquierda de la posición frente por haber sido eliminados algunos de sus elementos.

frente final

Existen diversas soluciones a este problema:

1.º Retroceso.

Consiste en mantener fijo a 1 el valor de frente, realizando un desplazamiento de una posición para todas

las componentes ocupadas cada vez que se efectúa una supresión.

P:63

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 467

2.º Reestructuración.

Cuando final llega al máximo de elementos se desplazan las componentes ocupadas hacia atrás las posiciones necesarias para que el principio coincida con el principio de la tabla.

3.º Mediante un array circular.

Un array circular es aquel en el que se considera que la componente primera sigue a la componente última.

Esta implementación obliga a dejar siempre una posición libre para separar el principio y el final del array.

Evidentemente, seguirá existiendo la limitación de que pueda llenarse completamente el array, Max-1

posiciones ocupadas.

12.8.2. Aprovechamiento de la memoria

El mejor método para evitar el desaprovechamiento de espacio es el diseño de la cola mediante un array circular.

yyy

xxx

Max

5

4

3

2

1

final ← 6

frente 4 ← frente 4 ←

frente 5 ←

frente 6 ←

final 1 ← final 1 ← final 1 ←

final 1 ←

frente 1 ←

1.o

Imaginamos

la siguiente

situación de partida

2.o

Introducción

de un nuevo

elemento

3.o

Extracción

de un

elemento

4.o

Extracción

de otro

5.o

Extracción

del último,

cola vacía

yyy

xxx

zzz

yyy

zzz

xxx

zzz

xxx

yyy

xxx

zzz

yyy

Deberemos efectuar las siguientes declaraciones:

const Max = <expresión>

tipo

registro : tipo_elemento

... : ...

... : ...

fin_registro

array[1..Max] de tipo_elemento : arr

var

entero : frente, final

arr : c

tipo_elemento : elemento

En c[frente] estará siempre libre, sirviendo para separar el principio y el final del array.

procedimiento inicializar(S entero: frente,final)

inicio

frente ← 1

final ← 1

fin_procedimiento

P:64

468 Fundamentos de programación

Los elementos se añaden por el final.

procedimiento meter(E/S entero:final; E/S arr: c;E tipo_elemento: elemento)

inicio

final ← final mod max + 1

c[final] ← elemento

fin_procedimiento

Los elementos se eliminan por el frente. El elemento a eliminar se encuentra siempre en la posición del array

siguiente a la especificada por frente.

procedimiento sacar(E/S entero: frente; E arr: c;

S tipo_elemento: elemento)

inicio

elemento ← c[frente mod max + 1]

frente ← frente mod max + 1

fin_procedimiento

logico función vacia(E entero: frente,final)

inicio

si frente = final entonces

devolver(verdad)

si_no

devolver(falso)

fin_si

fin_función

Cuando la posición siguiente a final sea frente no podremos añadir más información, pues habría que hacerlo en

c[final mod max + 1] es decir c[frente]

y la cola se encontrará llena

logico función llena(E entero: frente,final)

inicio

si frente = (final mod Max + 1!) entonces

devolver(verdad)

si_no

devolver(falso)

fin_si

fin_función

procedimiento consultarPrimero(E entero: frente; E arr: c; S tipo_elemento: elemento)

inicio

elemento ← c[frente mod max + 1]

fin_procedimiento

Con esta estrategia, array circular, se sacrifica un elemento del array para distinguir la cola llena de cola vacía.

12.9. DOBLE COLA

Existe una variante de la cola simple estudiada anteriormente y que es la doble cola. La doble cola o bicola es una

cola bidimensional en la que las inserciones y eliminaciones se pueden realizar en cualquiera de los dos extremos de

la lista.

P:65

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 469

Frente Fondo

Eliminación

Eliminación

Inserción

Inserción

Figura 12.18. Doble cola (bicola).

Existen dos variantes de la doble cola:

• Doble cola de entrada restringida: acepta inserciones sólo al final de la cola.

• Doble cola de salida restringida: acepta eliminaciones sólo al frente de la cola.

Inserción

IZQ = 5

DCHA = 25

Eliminación

x

5 25

x

Los procedimientos de inserción y eliminación de las dobles colas son variantes de los procedimientos estudiados

para las colas simples y se dejan como ejercicio al lector.

ACTIVIDADES DE PROGRAMACIÓN RESUELTAS

12.1. Una tienda de artículos deportivos desea almacenar en una lista enlazada, con un único elemento por producto, la

siguiente información sobre las ventas realizadas: Código del artículo, Cantidad y Precio. Usando estructuras de

tipo array, desarrollar un algoritmo que permita tanto la creación de la lista como su actualización al realizarse

nuevas ventas o devoluciones de un determinado producto.

Análisis del problema

El algoritmo contemplará la creación de la lista y colocará los elementos clasificados por código para que las búsquedas

puedan resultar algo más rápidas. Al producirse una venta se han de considerar las siguientes posibilidades:

• Es la primera vez que se vende ese artículo y esto nos lleva a la inserción de un nuevo elemento en la lista.

• Ya se ha vendido alguna otra vez dicho artículo; por tanto, es una modificación de un elemento de la lista, incrementándose la cantidad vendida.

Una devolución nos hará pensar en las siguientes situaciones:

• El comprador devuelve parte de lo que se había vendido de un determinado artículo, lo que representa una modificación de la cantidad vendida, decrementándose con la devolución.

• Se devuelve todo lo que se lleva vendido de un determinado artículo y, en consecuencia, el producto debe desaparecer de la lista de ventas.

Diseño del algoritmo

algoritmo ejercicio_12_1

const

max = ...

tipo

registro : tipo_elemento

cadena: cod

entero: cantidad

real: precio

fin_registro

P:66

470 Fundamentos de programación

registro : tipo_nodo

tipo_elemento: elemento

entero: sig

fin_registro

array[1..Max] de tipo_nodo: lista

var

entero: inic,vacio

lista: m

carácter: opcion

inicio

iniciar(m, vacio)

inicializar(inic)

repetir

escribir('1.- Ventas')

escribir('2.- Devoluciones')

escribir('3.- Mostrar lista')

escribir('4.- Fin')

escribir('Elija opcion')

leer(opcion)

según_sea opcion hacer

'1':nuevasventas(inic,vacio,m)

'2':devoluciones(inic,vacio,m)

'3':recorrer(inic,m)

fin_según

hasta_que opcion='4'

fin

procedimiento inicializar(S entero: inic);

inicio

inic ← 0

fin_procedimiento

lógico función vacia(E entero: inic)

inicio

devolver(inic = 0)

fin_función

procedimiento iniciar( E/S lista: m; E/S entero: vacio)

var

entero: i

inicio

vacio ← 1

desde i ← 1 hasta Max-1 hacer

m[i].sig ← i+1

fin_desde

m[Max].sig ← 0

fin_procedimiento

procedimiento reservar(S entero: auxi; E lista: m; E/S entero: vacio)

inicio

si vacio = 0 entonces

escribir('Memoria agotada')

auxi ← 0

si_no

auxi ← vacio

vacio ← m[vacio].sig

fin_si

fin_procedimiento

P:67

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 471

lógico funcion llena(E entero: vacio)

inicio

devolver(vacio = 0)

fin_función

procedimiento consultar(E entero: inic; S entero: posic, anterior;

E tipo_elemento: elemento; S lógico: encontrado;

E lista: m)

inicio

si no vacia (inic) entonces

anterior ← 0

posic ← inic

mientras (m[posic].elemento.cod < elemento.cod) y (posic<>0) hacer

anterior ← posic

posic ← m[posic].sig

fin_mientras

si m[posic].elemento.cod = elemento.cod entonces

encontrado ← verdad

si_no

encontrado ← falso

fin_si

fin_si

fin_procedimiento

procedimiento insertar(E/S entero: inic,anterior;

E tipo_elemento: elemento;

E/S lista: m; E/S entero: vacio)

var

entero: auxi

inicio

si no llena (vacio) entonces

reservar(auxi,m,vacio)

m[auxi].elemento ← elemento

si anterior=0 entonces

m[auxi].sig ← inic

inic ← auxi

si_no

m[auxi].sig ← m[anterior].sig

m[anterior].sig ← auxi

fin_si

anterior ← auxi

fin_si

fin_procedimiento

procedimiento escribir_reg(E tipo_elemento: e)

inicio

escribir(e.cod)

escribir(e.cantidad)

escribir(e.precio)

fin_procedimiento

procedimiento recorrer(E entero: inic; E lista: m)

var

entero: posic

inicio

posic ← inic

mientras posic<>0 hacer

escribir_reg(m[posic].elemento)

posic ← m[posic].sig

fin_mientras

fin_procedimiento

P:68

472 Fundamentos de programación

procedimiento liberar(E/S entero: posic; E/S lista: m;

E/S entero: vacio)

inicio

m[posic].sig ← vacio

vacio ← posic

fin_procedimiento

procedimiento suprimir(E/S entero: inic, anterior, posic;

E/S lista: m; E/S entero: vacio)

inicio

si anterior=0 entonces

inic ← m[posic].sig

si_no

m[anterior].sig ← m[posic].sig

fin_si

liberar(posic,m,vacio)

anterior ← 0

posic ← inic

fin_procedimiento

procedimiento nuevasventas(E/S entero: inic, vacio; E/S lista: m)

var

tipo_elemento: elemento

lógico: encontrado

entero: anterior, posic

inicio

repetir

escribir('Introduzca * en el código para terminar')

escribir('Código: ')

leer(elemento.cod)

si elemento.cod <>'*' entonces

si vacia(inic) entonces

anterior ← 0

escribir('Cantidad: ')

leer(elemento.cantidad)

escribir('Precio: ')

leer(elemento.precio)

insertar(inic,anterior,elemento,m,vacio)

si_no

consultar(inic,posic,anterior,elemento,encontrado,m)

si no encontrado entonces

si no llena(vacio) entonces

escribir('Cantidad: ')

leer(elemento.cantidad)

escribir('Precio: ')

leer(elemento.precio)

insertar(inic,anterior,elemento,m,vacio)

si_no

escribir('Llena')

fin_si

si_no

escribir('Cantidad: ')

leer(elemento.cantidad)

m[posic].elemento.cantidad ← m[posic].elemento.cantidad+

elemento.cantidad

fin_si

fin_si

fin_si

hasta_que elemento.cod = '*'

fin_procedimiento

P:69

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 473

procedimiento devoluciones( E/S entero: inic, vacio; E/S lista: m)

var

tipo_elemento: elemento

entero : posic, anterior

entero : cantidad

lógico : encontrado

inicio

si no vacia(inic) entonces

escribir('Introduzca un * en el código para terminar')

escribir('Código: ')

leer(elemento.cod)

si_no

escribir('No hay ventas, no puede haber devoluciones')

fin_si

mientras (elemento.cod<>'*' ) y no vacia(inic) hacer

consultar(inic, posic, anterior, elemento, encontrado, m)

si encontrado entonces

repetir

escribir('Deme cantidad devuelta ')

leer(cantidad)

si cantidad > m[posic].elemento.cantidad entonces

escribir('Error')

fin_si

hasta_que cantidad <= m[posic].elemento.cantidad

m[posic].elemento.cantidad ← m[posic].elemento.cantidad-cantidad

si m[posic].elemento.cantidad=0 entonces

suprimir(inic,anterior,posic,m,vacio)

fin_si

si_no

escribir('No existe')

fin_si

si no vacia(inic) entonces

escribir('Introduzca un * en el código para terminar')

leer(elemento.cod)

si_no

escribir('No hay ventas, no puede haber devoluciones')

fin_si

fin_mientras

fin_procedimiento

12.2. Diseñar un procedimiento que realice una copia de una pila en otra.

Análisis del problema

Entendemos por copiar la acción de rellenar otra pila con los mismos elementos y en el mismo orden. Por lo tanto, si simplemente sacamos los elementos de la pila y los metemos en otra, tendrá los mismos elementos, pero en orden distinto.

Tenemos dos soluciones, una sería utilizar una pila auxiliar: sacaremos los elementos de la pila principal y los meteremos

en la auxiliar, para después volcarlos en dos pilas, una de las cuales es la de salida. La otra solución sería recursiva: se

sacan los elementos de la pila mediante llamadas recursivas; cuando la pila esté vacía inicializaremos la copia y a la vuelta de la recursividad se van introduciendo los elementos en dos pilas en orden inverso a como han salido.

Observe que el procedimiento valdrá tanto para la implementación con arrays como con estructuras dinámicas de datos.

Diseño del algoritmo

Solución iterativa

procedimiento CopiarPila(E/S pila: p; S pila: copia)

var

pila: aux

tipo_elemento: e

P:70

474 Fundamentos de programación

inicio

PilaVacia(aux)

mientras no EsPilaVacia(p) hacer

Tope(p,e) "extra el primer elemento sin borrado

PInsertar(aux,e) "inserta elemento

PBorrar(p) "borra elemento

fin_mientras

PilaVacia(copia)

mientras no EsPilaVacia(aux) hacer

Tope(aux,e)

PInsertar(copia, e)

PInsertar(p, e)

PBorrar(aux)

fin_mientras

fin_procedimiento

Solución recursiva (la recursividad se estudia en el Capítulo 14)

procedimiento CopiarPilaR(E/S pila: p, copia)

var

tipo_elemento: e

inicio

si no EsPilaVacia(p) entonces

Tope(p,e)

PBorrar(p)

CopiarPila(p, copia)

PInsertar(copia, e)

Pinsertar(p, e)

si_no

PilaVacia(copia)

fin_si

fin_procedimiento

Los procedimientos y funciones utilizados implementados con punteros son

procedimiento PilaVacia(S pila: p)

inicio

p ← nulo

fin_procedimiento

lógico function EsPilaVacia(E pila: p)

inicio

devolver(p = nulo)

fin_función

procedimiento PInsertar(E/S pila: p; E tipo_elemento: e)

var

pila: aux

inicio

reservar(aux)

aux→.info ← e

aux→.cimaant ← p

p ← aux

fin_procedimiento

procedimiento PBorrar(E/S pila: p)

var

pila: aborrar

P:71

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 475

inicio

Si no EsPilaVacia (p) entonces

aborrar ← p

p ← p→.cimaant

liberar(aborrar)

fin_sin

fin_procedimiento

procedimiento Tope(E pila: p; S tipo_elemento: e)

inicio

e ← p→.info

fin_procedimiento

12.3. Diseñar un procedimiento que elimine el elemento enésimo de una pila.

Análisis del problema

También en este caso se debe utilizar una pila auxiliar o recursividad para poder restaurar los elementos en el mismo orden.

Es necesario borrar elementos e insertarlos en la pila auxiliar hasta llegar al elemento n. En ese punto, se sacan todos los

elementos de la pila auxiliar y se introducen en la pila original. Obsérvese que también en este caso es totalmente indistinto utilizar estructuras de datos dinámicas o estáticas.

Diseño del algoritmo

Solución iterativa

procedimiento BorrarElementoN(E/S pila: p; E entero: n)

var

pila: aux

tipo_elemento: e

entero: i

inicio

i ← 1

PilaVacia(aux)

mientras no EsPilaVacia(p) y (i<n) hacer

i ← i+1

Tope(p, e)

PInsertar(aux, e)

PBorrar(p)

fin_mientras

Pborrar(p)

mientras no EsPilaVacia(aux) hacer

Tope(aux, e)

PInsertar(p, e)

PBorrar(aux)

fin_mientras

fin_procedimiento

Solución recursiva

procedimiento BorrarElementoN(E/S pila: p; E entero: n)

var

tipo_elemento: e

inicio

si (n>1) y no EsPilaVacia(p) entonces

tope(p,e)

PBorrar(p)

BorrarElementoN(p, n-1)

PInsertar(p,e)

si_no

PBorrar(p)

fin_si

fin_procedimiento

P:72

476 Fundamentos de programación

12.4. Diseñar un algoritmo para, utilizando pilas y colas, comprobar si una frase es un palíndromo (un palíndromo es

una frase que se lee igual de izquierda a derecha que de derecha a izquierda).

Análisis del problema

Se puede aprovechar el distinto orden en que salen los elementos de una pila y una cola para averiguar si una frase es igual

a ella misma invertida. Para ello, una vez introducida la frase, se insertan en una pila y una cola todos los caracteres (se evitarán los signos de puntuación, y se podría mejorar si se convierten todos los caracteres a mayúsculas o minúsculas y se eliminan los acentos).

A continuación se van sacando elementos de la pila y de la cola. Si aparece algún carácter distinto, es que la frase no

es igual a ella misma invertida y, por lo tanto, no será un palíndromo. Si al acabar de sacar los elementos, todos han sido

iguales, se trata de un palíndromo.

Diseño del algoritmo

algoritmo ejercicio_12_4;

{Aquí deberían incluirse las declaraciones, procedimientos y funciones

para trabajar con pilas y colas.

Es indistinto trabajar con estructuras estáticas o dinámicas}

var

pila : p

cola : c

cadena : car1, car2, frase

entero : i

inicio

ColaVacia(c)

PilaVacia(p)

leer(frase)

desde i ← 1 hasta longitud(frase) hacer

car1 ← subcadena(frase, i, 1)

{si no es un signo de puntuación}

si posicion(car1,',:;.')= 0 entonces

CInsertar(c,car1)

PInsertar(p,car1)

fin_si

fin_desde

repetir

Primero(c,car1)

Tope(p,car2)

PBorrar(p)

CBorrar(c)

hasta_que (car1<>car2) o EsColaVacia(c)

si car1=car2 entonces

escribir('Es un palíndromo')

si_no

escribir('No es un palíndromo')

fin_si

fin

CONCEPTOS CLAVE

• Apuntador.

• Cola.

• Doble cola.

• Enlace.

• Estructura de datos dinámica.

• Estructura de datos estática.

• Lista circular.

• Lista doblemente enlazada.

• Lista enlazada.

• Pila.

• Puntero.

P:73

Estructuras dinámicas lineales de datos (pilas, colas y listas enlazadas) 477

RESUMEN

Una lista lineal es una lista en la que cada elemento tiene

un único sucesor. Las operaciones típicas en una lista lineal

son: inserción, supresión, recuperación y recorrido.

Una lista enlazada es una colección ordenada de datos

en los que cada elemento contiene la posición (dirección)

del siguiente elemento. Es decir, cada elemento (nodo) de

la lista contiene dos parte: datos y enlace (puntero).

Una lista simplemente enlazada contiene sólo un enlace a un sucesor único a menos que sea el último, en cuyo

caso no se enlaza con ningún otro nodo. Cuando se desea

insertar un elemento en una lista enlazada, se deben considerar dos casos: añadir al principio y añadir en el interior o

añadir al final. Si se desea eliminar un nodo de una lista se

deben considerar dos casos: eliminar el primer nodo y eliminar cualquier otro nodo. El recorrido de una lista enlazada implica visitar cada nodo de la lista y procesar en su caso.

Una lista doblemente enlazada es una lista en la que

cada nodo tiene un puntero a su sucesor y otro a su predecesor. Una lista enlazada circularmente es una lista en

la que el enlace del último nodo apunta al primero de la

lista.

Una pila es una estructura de datos tipo LIFO (last-in,

first-out, último en entrar, primero en salir) en la que los

datos se insertan y eliminan por el mismo extremo que se

denomina cima de la pila. Se definen diferentes operaciones: crear, apilar, desapilar, pilaVacía, pilaLlena, cimaPila.

Una cola (FIFO, first-in, first-out) es una lista lineal en

la que los datos se pueden insertar por un extremo denominado Cabeza y se elimina o borra por el otro extremo denominado Cola o Final. Las operaciones básicas de una

cola son: poner, quitar, frenteCola y Colavacia,

Colallena.

Las pilas y las colas se pueden implementar mediante

arrays y mediante listas enlazadas.

EJERCICIOS

12.1. Dada una lista lineal cuya estructura de nodos consta de los campos INFO y ENLACE, diseñar un algoritmo que cuente el número de nodos de la lista.

12.2. Diseñar un algoritmo que cambie el campo INFO del

n-ésimo nodo de una lista enlazada simple por un

valor dado x.

12.3. Dadas dos listas enlazadas, cuyos nodos frontales se

indican por los apuntadores PRIMERO y SEGUNDO,

respectivamente, realizar un algoritmo que una ambas listas. El nodo frontal de la lista nueva se almacenará en TERCERO.

12.4. Se dispone de una lista enlazada DEMO 1 almacenada

en memoria. Realizar un algoritmo que copie la lista DEMO 1 en otra denominada DEMO 2.

12.5. Escribir un algoritmo que realice una inserción contigua a la izquierda del n-ésimo nodo de una lista

enlazada y repetir el ejercicio para una inserción

también contigua a la derecha de n-ésimo nodo.

12.6. Escribir un algoritmo que divida una lista enlazada

determinada en dos listas enlazadas independientes.

El primer nodo de la lista principal es PRIMERO y la

variable PARTIR es la dirección del nodo que se convierte en el primero de los nodos de la segunda lista

enlazada resultante.

12.7. Como aplicación de pilas, obtener un subalgoritmo

función recursiva de la función de Ackermann.

Función de Ackermann

A(m, n) = { n + 1 si m = 0

A(m – 1,1) si n = 0

A(m – 1) A(m, n – 1) restantes casos

12.8. Escribir un subalgoritmo que permita insertar un

elemento en una doble cola —representada por un

vector—. Tengan en cuenta que debe existir un parámetro que indique el extremo de la doble cola en

que debe realizarse la inserción.

12.9. Realizar un algoritmo que cuente el número de

nodos de una lista circular que tiene una cabecera.

12.10. Diseñar un algoritmo que inserte un nodo al final

de una lista circular.

P:75

CAPÍTULO 13

Estructura de datos no lineales

(árboles y grafos)

13.1. Introducción

13.2. Árboles

13.3. Árbol binario

13.4. Árbol binario de búsqueda

13.5. Grafos

ACTIVIDADES DE PROGRAMACIÓN RESUELTAS

CONCEPTOS CLAVE

RESUMEN

EJERCICIOS

Las estructuras dinámicas lineales de datos —listas

enlazadas, pilas y colas— tienen grandes ventajas de

flexibilidad sobre las representaciones contiguas; sin

embargo, tienen un punto débil: son listas secuenciales, es decir, están dispuestas de modo que es necesario moverse a través de ellas una posición cada vez

(cada elemento tiene un siguiente elemento). Esta linealidad es típica de cadenas, de elementos que pertenecen a una sola dimensión: campos en un registro,

entradas en una pila, entradas en una cola y de nodos

en una lista enlazada simple. En este capítulo se tratarán las estructuras de datos no lineales que resuelven los problemas que plantean las listas lineales y en

las que cada elemento puede tener diferentes “siguientes” elementos, que introducen el concepto de

estructuras de bifurcación. Estos tipos de datos se llaman árboles.

Asimismo, este capítulo introduce a una estructura

matemática importante que tiene aplicaciones en

ciencias tan diversas como la sociología, química, física, geografía y electrónica. Estas estructuras se denominan grafos.

INTRODUCCIÓN

P:76

480 Fundamentos de programación

13.1. INTRODUCCIÓN

Las estructuras de datos que han sido examinadas hasta ahora en este libro son lineales. A cada elemento le correspondía siempre un “siguiente” elemento. La linealidad es típica de cadenas, de elementos de arrays o listas, de campos en registros, entradas en pilas o colas y nodos en listas enlazadas.

En este capítulo se examinarán las estructuras de datos no lineales. En estas estructuras cada elemento puede

tener diferentes “siguientes” elementos, que introduce el concepto de estructuras de bifurcación.

Las estructuras de datos no lineales son árboles y grafos. A estas estructuras se les denomina también estructuras

multienlazadas.

13.2. ÁRBOLES

El árbol es una estructura de datos fundamental en informática, muy utilizada en todos sus campos, porque se adapta a la representación natural de informaciones homogéneas organizadas y de una gran comodidad y rapidez de manipulación. Esta estructura se encuentra en todos los dominios (campos) de la informática, desde la pura algorítmica

(métodos de clasificación y búsqueda...) a la compilación (árboles sintácticos para representar las expresiones o producciones posibles de un lenguaje) o incluso los dominios de la inteligencia artificial (árboles de juegos, árboles de

decisiones, de resolución, etc.).

Las estructuras tipo árbol se usan principalmente para representar datos con una relación jerárquica entre sus

elementos, como son árboles genealógicos, tablas, etc.

Un árbol A es un conjunto finito de uno o más nodos, tales que:

1. Existe un nodo especial denominado RAIZ(v1) del árbol.

2. Los nodos restantes (v2, v3, ..., vn) se dividen en m >= 0 conjuntos disjuntos denominado A1, A2, ..., Am, cada

uno de los cuales es, a su vez, un árbol. Estos árboles se llaman subárboles del RAIZ.

La definición de árbol implica una estructura recursiva. Esto es, la definición del árbol se refiere a otros árboles.

Un árbol con ningún nodo es un árbol nulo; no tiene raíz.

La Figura 13.1 muestra un árbol en el que se ha rotulado cada nodo con una letra dentro de un círculo. Esta es

una notación típica para dibujar árboles.

Los tres subárboles del raíz A son B, C y D, respectivamente. B es la raíz de un árbol con un subárbol E. Este

subárbol no tiene subárbol conectado. El árbol C tiene dos subárboles, F y G.

!

x n

+ log

a b

a *

b c

o

< <

a b c d

Figura 13.1. Diferentes árboles.

P:77

Estructura de datos no lineales (árboles y grafos) 481

13.2.1. Terminología y representación de un árbol general

La representación y terminología de los árboles se realiza con las típicas notaciones de las relaciones familiares

en los árboles genealógicos: padre, hijo, hermano, ascendente, descendiente, etc. Sea el árbol general de la Figura 13.2.

K L

F G

E

HI J

B C D

A

Figura 13.2. Árbol general.

Las definiciones a tener en cuenta son:

• Raíz del árbol. Todos los árboles que no están vacíos tienen un único nodo raíz. Todos los demás elementos o

nodos se derivan o descienden de él. El nodo raíz no tiene padre, es decir, no es el hijo de ningún elemento.

• Nodo, son los vértices o elementos del árbol.

• Nodo terminal u hoja (leaf node) es aquel nodo que no contiene ningún subárbol (los nodos terminales u hojas

del árbol de la Figura 13.2 son E, F, K, L, H y J).

• A cada nodo que no es hoja se asocia uno o varios subárboles llamados descendientes (offspring) o hijos. De

igual forma, cada nodo tiene asociado un antecesor o ascendiente llamado padre.

• Los nodos de un mismo padre se llaman hermanos.

• Los nodos con uno o dos subárboles —no son hojas ni raíz— se llaman nodos interiores o internos.

• Una colección de dos o más árboles se llama bosque (forest).

• Todos los nodos tienen un solo padre —excepto el raíz— que no tiene padre.

• Se denomina camino el enlace entre dos nodos consecutivos y rama es un camino que termina en una hoja.

• Cada nodo tiene asociado un número de nivel que se determina por la longitud del camino desde el raíz al nodo

específico. Por ejemplo, en el árbol de la Figura 13.2.

Nivel 0 A

Nivel 1 B, C, D

Nivel 2 E, F, G, H, I, J

Nivel 3 K, L

• La altura o profundidad de un árbol es el número máximo de nodos de una rama. Equivale al nivel más alto

de los nodos más uno. El peso de un árbol es el número de nodos terminales. La altura y el peso del árbol de

la Figura 13.2 son 4 y 7, respectivamente.

Las representaciones gráficas de los árboles —además de las ya expuestas— pueden ser las mostradas en la Figura 13.3.

P:78

482 Fundamentos de programación

13.3. ÁRBOL BINARIO

Existe un tipo de árbol denominado árbol binario que puede ser implementado fácilmente en una computadora.

Un árbol binario es un conjunto finito de cero o más nodos, tales que:

• Existe un nodo denominado raíz del árbol.

• Cada nodo puede tener 0, 1 o 2 subárboles, conocidos como subárbol izquierdo y subárbol derecho.

La Figura 13.4 representa diferentes tipos de árboles binarios:

B

A

+

C

/

T T1 T2

100

5

5

100

(a) (b) (c)

Figura 13.4. Ejemplos de árboles binarios: (a) expresión árbol a + b/c;

(b) y (c) dos árboles diferentes con valores enteros.

K L

F H G I J

B C D

A

E

A

E

B J I

D H

F

K

L

C G

Figura 13.3 Representaciones de árboles.

P:79

Estructura de datos no lineales (árboles y grafos) 483

13.3.1. Terminología de los árboles binarios

Dos árboles binarios se dice que son similares si tienen la misma estructura, y son equivalentes si son similares y

contienen la misma información (Figura 13.5).

Un árbol binario está equilibrado si las alturas de los dos subárboles de cada nodo del árbol se diferencian en una

unidad como máximo.

altura (subárbol izquierdo) – altura (subárbol derecho) ≤ 1

(a)

B

C

F

C

E

(b)

C

Q

X

A

T

G

A

Figura. 13.5. Árboles binarios: (a) similares, (b) equivalentes.

El procesamiento de árboles binarios equilibrados es más sencillo que los árboles no equilibrados. En la Figura 13.6 se muestran dos árboles binarios de diferentes alturas y en la Figura 13.7, árboles equilibrados y sin equilibrar.

T T2

F

(a) (b) (c)

–22

14

/

7

T1

E

D

C

B

A

Figura 13.6. Árboles binarios de diferentes alturas: (a) altura 3, (b) árbol vacío, altura 0, (c) altura 6.

P:80

484 Fundamentos de programación

(a) (b)

g

j

h

d

i

f

e

b

a

1

2

1

2

3

4

5

1

Figura 13.7. Arboles binarios: (a) equilibrados, (b) no equilibrados.

13.3.2. Árboles binarios completos

Un árbol binario se llama completo si todos sus nodos tienen exactamente dos subárboles, excepto los nodos de los

niveles más bajos que tienen cero. Un árbol binario completo, tal que todos los niveles están llenos, se llama árbol

binario lleno.

En la Figura 13.8 se ilustran ambos tipos de árboles.

Un árbol binario T de nivel h puede tener como máximo 2h – 1 nodos.

La altura de un árbol binario lleno de n nodos es log2(n + 1). A la inversa, el número máximo de nodos de un

árbol binario de altura h será 2h – 1. En la Figura 13.9 se muestra la relación matemática que liga los nodos de

un árbol.

Por último, se denomina árbol degenerado un árbol en el que todos sus nodos tienen solamente un subárbol,

excepto el último.

(a)

L

M

N

I O

J

K

D

E

F

A G

B

C

(b)

E

A

B

C

D

T

H

T

Figura 13.8. (a) árbol binario lleno de altura 4, (b) árbol binario completo de altura 3.

P:81

Estructura de datos no lineales (árboles y grafos) 485

D

E

F

A G

B

C

T

Número de nodos (N) = 7 = 2h

– 1

Altura (H) = 3

Nivel 3; número de nodos = 2(3–1) = 4

Figura 13.9. Relaciones matemáticas de un árbol binario.

13.3.3. Conversión de un árbol general en árbol binario

Dado que los árboles binarios es la estructura fundamental en la teoría de árboles, será preciso disponer de algún

mecanismo que permita la conversión de un árbol general en un árbol binario.

Los árboles binarios son más fáciles de programar que los árboles generales. En éstos es imprescindible deducir

cuántas ramas o caminos se desprenden de un nodo en un momento dado. Por ello, y dado que de los árboles binarios

siempre se cuelgan como máximo dos subárboles, su programación será más sencilla.

Afortunadamente existe una técnica para convertir un árbol general a formato de árbol binario. Supongamos que

se tiene el árbol A y se quiere convertir en un árbol binario B. El algoritmo de conversión tiene tres pasos fáciles:

1. La raíz de B es la raíz de A.

2. a) Enlazar al nodo raíz con el camino que conecta el nodo más a la izquierda (su hijo).

b) Enlazar este nodo con los restantes descendientes del nodo raíz en un camino, con lo que se forma el

nivel 1.

c) A continuación, repetir los pasos a) y b) con los nodos del nivel 2, enlazando siempre en un mismo camino todos los hermanos —descendientes del mismo nodo—. Repetir estos pasos hasta llegar al nivel

más alto.

3. Girar el diagrama resultante 45° para diferenciar entre los subárboles izquierdo y derecho.

T

A

B

C

D

E

T

A

C

E

B

D

Figura 13.10. Árboles degenerados.

P:82

486 Fundamentos de programación

EJEMPLO 13.1

Convertir el árbol general T en un árbol binario.

K L

F HI J G

B C D

A

E

Siguiendo los pasos del algoritmo.

Paso 1:

K L

F H G I J

B C D

A

E

Paso 2:

A

B

E

C

F D

G

K

L

H

I

J

P:83

Estructura de datos no lineales (árboles y grafos) 487

Paso 3:

Obsérvese que no existe camino entre E y F, debido a que no son descendientes del árbol original, ya que ellos tienen

diferentes padres B y C.

En el árbol binario resultante los punteros izquierdos son siempre de un nodo padre a su primer hijo (más a la

izquierda) en el árbol general original. Los punteros derechos son siempre desde un nodo de sus descendientes en el

árbol original.

EJEMPLO 13.2

El algoritmo de conversión puede ser utilizado para convertir un bosque de árboles generales a un solo árbol binario.

El bosque siguiente puede ser representado por un árbol binario.

Bosque de árboles:

F

D

C

B

A

E G

H

I

K

J

L

M N O P

Árbol binario equivalente

A

B

N

O

P

C

D

E

F

H G

K

I

J

L

M

P:84

488 Fundamentos de programación

EJEMPLO 13.3

Convertir el árbol general en árbol binario.

d

h i

j

c

a

e g

f

b

Solución

Paso 1:

b

a

Paso 2:

d

h i

j

c

a

e g

f

b

Paso 3:

a

b

e

c

d

g

h

i

J

f

P:85

Estructura de datos no lineales (árboles y grafos) 489

13.3.4. Representación de los árboles binarios

Los árboles binarios pueden ser representados de dos modos diferentes:

• Mediante punteros (lenguajes C y C++).

• Mediante arrays o listas enlazadas.

• Vinculando nodos, objetos con mienbros que referencian otros objetos del mismo tipo.

13.3.4.1. Representación por punteros

Cada nodo de un árbol será un registro que contiene al menos tres campos:

• Un campo de datos con un tipo de datos.

• Un puntero al nodo del subárbol izquierdo (que puede ser nulo-null).

• Un puntero al nodo del subárbol derecho (que puede ser nulo-null).

CLAVE

CLAVE CLAVE

NULO

Figura 13.11. Representación de un árbol con punteros.

Ramón

Josefina Andrés

Miguel Laura

Manuel Erika

Ana Pascal

Koldo Clemente

En lenguaje algorítmico se tendrá:

tipo nodo_arbol

puntero_a nodo_arbol: punt

registro : nodo_arbol

P:86

490 Fundamentos de programación

<tipo_elemento> : elemento

punt: subiz, subder

fin_registro

13.3.4.2. Representación por listas enlazadas

Mediante una lista enlazada se puede siempre representar el árbol binario de la Figura 13.12.

A

B

D

C

E F

G

Figura 13.12. Árbol binario.

Nodo del árbol: campo 1 INFO (nodo)

campo 2 IZQ (nodo)

campo 3 DER (nodo)

El árbol binario representado como una lista enlazada se representa en la Figura 13.13.

NULO D NULO

B NULO C

A

IZDA INFO DCHA

NULO E NULO F NULO

NULO G NULO

Figura 13.13. Árbol binario como lista enlazada.

13.3.4.3. Representación por arrays

Existen diferentes métodos; uno de los más fáciles es mediante tres arrays lineales paralelos que contemplan el campo de información y los dos punteros de ambos subárboles. Así, por ejemplo, el nodo raíz RAMÓN tendrá dos punteros

P:87

Estructura de datos no lineales (árboles y grafos) 491

IZQ:(9) —JOSEFINA— y DER:(16) —ANDRÉS—, mientras que el nodo CLEMENTE, al no tener descendientes, sus

punteros se consideran cero (IZQ:0, DER:0).

3 Ramón

9 Josefina Andrés

6 Miguel 7 Laura

12 Manuel 11 Erika

15 Ana 4 Pascal

16

8 Koldo 13 Clemente

16

15

14

13

12

11

10

9

8

7

6

5

4

2

1

ANDRÉS

ANA

CLEMENTE

MANUEL

ERIKA

JOSEFINA

KOLDO

LAURA

MIGUEL

PASCAL

RAMÓN

INFO

3

P

IZQ. DER.

9

15

8

0

0

0

6

0

11

12

0

16

4

0

0

0

0

7

0

0

0

13

Figura 13.14. Árbol binario como arrays.

Otro método resulta más sencillo —un array lineal—. Para ello se selecciona un array lineal ARBOL.

50

21

12

75

32 90

16 25 85

P:88

492 Fundamentos de programación

El algoritmo de transformación es:

1. La raíz del árbol se guarda en ARBOL [1].

2. si un nodo n está en ARBOL[i] entonces

su hijo izquierdo se pone en ARBOL[2*i]

y su hijo derecho en ARBOL[2*i + 1]

si un subárbol está vacío, se le da el valor NULO.

Este sistema requiere más posiciones de memoria que nodos tiene el árbol. Así, la transformación necesitará un

array con 2h + 2 elementos si el árbol tiene una profundidad h. En nuestro caso, como la profundidad es 3, requerirá

32 posiciones (25

), aunque si no se incluyen las entradas nulas de los nodos terminales, veremos cómo sólo necesita

catorce posiciones.

Árbol

1

.

.

.

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

50

21

75

12

32

0

90

0

16

25

85

P:89

Estructura de datos no lineales (árboles y grafos) 493

Un tercer método, muy similar al primero, sería la representación mediante un array de registros.

ANDRÉS

ANA

CLEMENTE

MANUEL

ERIKA

JOSEFINA

KOLDO

LAURA

MIGUEL

PASCAL

RAMÓN

INFO IZQ DER

9

0

12

11

0

6

0

0

0

8

15

16

13

0

0

0

7

0

0

0

0

4

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

P 3

13.3.5. Recorrido de un árbol binario

Se denomina recorrido de un árbol el proceso que permite acceder una sola vez a cada uno de los nodos del árbol.

Cuando un árbol se recorre, el conjunto completo de nodos se examina.

Existen muchos modos para recorrer un árbol binario. Por ejemplo, existen seis diferentes recorridos generales

en profundidad de un árbol binario, simétricos dos a dos.

Los algoritmos de recorrido de un árbol binario presentan tres tipos de actividades comunes:

• Visitar el nodo raíz.

• Recorrer el subárbol izquierdo.

• Recorrer el subárbol derecho.

Estas tres acciones repartidas en diferentes órdenes proporcionan los diferentes recorridos del árbol en profundidad. Los más frecuentes tienen siempre en común recorrer primero el subárbol izquierdo y luego el subárbol derecho.

Los algoritmos que lo realizan llaman pre-orden, post-orden, in-orden y su nombre refleja el momento en que se

visita el nodo raíz. En el in-orden el raíz está en el medio del recorrido, en el pre-orden el raíz está el primero y en

el post-orden el raíz está el último:

Recorrido pre-orden

1. Visitar el raíz.

2. Recorrer el subárbol izquierdo en pre-orden.

3. Recorrer el subárbol derecho en pre-orden.

Recorrido in-orden

1. Recorrer el subárbol izquierdo en in-orden.

2. Visitar el raíz.

3. Recorrer el subárbol derecho en in-orden.

P:90

494 Fundamentos de programación

Recorrido post-orden

1. Recorrer el subárbol izquierdo en post-orden.

2. Recorrer el subárbol derecho en post-orden.

3. Visitar el raíz.

Obsérvese que todas estas definiciones tienen naturaleza recursiva.

En la Figura 13.15 se muestran los recorridos de diferentes árboles binarios.

Árbol 1: c * d + e

c

*

+

d

e g

e

f

+

c

/

d

/

a b

*

+

Árbol 2: [((a + b) * c/d) + e ^ f]/g

T Z

M

E

B L

A D

N

P

V

Árbol 3

Figura 13.15. Recorrido de árboles binarios.

Árbol 1 Pre-orden + * c d e

In-orden c * d + e

Post-orden c d * e +

Árbol 2 Pre-orden / + * + a b / c d ^ e f g

In-orden a + b * c / d + e ^ f / g

Post-orden a b + c d / * e f ^ + g /

Árbol 3 Pre-orden MEBADLPNVTZ

In-orden ABDELMNPTVZ

Post-orden ADBLENTZVPM

EJEMPLO 13.4

Calcular los recorridos del árbol binario.

*

(/)

(–)

c d

e

+

a b

Solución

recorrido pre-orden / * + ab - cde

recorrido in-orden a + b * c - d/e

recorrido post-orden ab + cd - * e/

P:91

Estructura de datos no lineales (árboles y grafos) 495

EJEMPLO 13.5

Realizar los recorridos del árbol binario.

2

5

1

8

4 10

7 12

Solución

recorrido pre-orden 2 5 1 7 4 8 10 12

recorrido in-orden 1 7 5 4 2 8 12 10

recorrido post-orden 7 1 4 5 12 10 8 2

13.4. ÁRBOL BINARIO DE BÚSQUEDA

Recordará del Capítulo 10, “Ordenación, búsqueda e intercalación”, que para localizar un elemento en un array se

podía realizar una búsqueda lineal; sin embargo, si el array era grande, una búsqueda lineal era ineficaz por su lentitud, especialmente si el elemento no estaba en el array, ya que requería la lectura completa del array. Se ganaba

tiempo si se clasificaba el array y se utilizaba una búsqueda binaria. Sin embargo, en un proceso de arrays las inserciones y eliminaciones son continuas, por lo que esto se hará complejo en cualquier método.

En los casos de gran número de operaciones sobre arrays o listas, lo que se necesita es una estructura donde los

elementos puedan ser eficazmente localizados, insertados o borrados. Una solución a este problema es una variante del árbol binario que se conoce como árbol binario de búsqueda o árbol binario clasificado (binary search

tree).

El árbol binario de búsqueda se construirá teniendo en cuenta las siguientes premisas:

• El primer elemento se utiliza para crear el nodo raíz.

• Los valores del árbol deben ser tales que pueda existir un orden (entero, real, lógico o carácter e incluso definido por el usuario si implica un orden).

• En cualquier nodo todos los valores del subárbol izquierdo del nodo son menor o igual al valor del nodo. De

modo similar, todos los valores del subárbol derecho deben ser mayores que los valores del nodo.

Si estas condiciones se mantienen, es sencillo probar que el recorrido in-orden del árbol produce los valores

clasificados por orden. Así, por ejemplo, en la Figura 13.16 se muestra un árbol binario.

Los tres recorridos del árbol son:

pre-orden P F B H G S R Y T W Z

in-orden B F G H P R S T Y W Z

post-orden B G H F R T W Z Y S P

En esencia, un árbol binario contiene una clave en cada nodo que satisface las tres condiciones anteriores.

Un árbol con las propiedades anteriores se denomina árbol binario de búsqueda.

P:92

496 Fundamentos de programación

P

F

B

S

H Y

G T

H

W

T

Figura 13.16. Árbol binario.

EJEMPLO 13.6

Se dispone de un array que contiene los siguientes caracteres:

D F E B A C G

Construir un árbol binario de búsqueda.

Los pasos para la construcción del algoritmo son:

1. Nodo raíz del árbol: D.

2. El siguiente elemento se convierte en el descendente derecho, dado que F alfabéticamente es mayor que D.

3. A continuación, se compara E con el raíz. Dado que E es mayor que D, pasará a ser un hijo de F y como

E < F será el hijo izquierdo.

4. El siguiente elemento B se compara con el raíz D y como B < D y es el primer elemento que cumple esta

condición, B será el hijo izquierdo de D.

5. Se repiten los pasos hasta el último elemento.

El árbol binario de búsqueda resultante sería:

D

B

A C

F

E G

EJEMPLO 13.7

Construir el árbol binario de búsqueda correspondiente a la lista de números.

4 19 -7 49 100 0 22 12

P:93

Estructura de datos no lineales (árboles y grafos) 497

El primer valor, como ya se ha comentado, es la raíz del árbol: es decir, 4. El siguiente valor, 19, se compara con

4; como es más grande se lleva al subárbol derecho de 4. El siguiente valor, –7, se compara con el raíz y es menor

que su valor, 4; por tanto, se mueve al subárbol izquierdo. La Figura 13.17 muestra los sucesivos pasos.

4

–7 19

0 49

22

12

100

Figura 13.17. Construcción de un árbol binario.

13.4.1. Búsqueda de un elemento

La búsqueda en un árbol binario ordenado es dicotómica, ya que a cada examen de un nodo se elimina aquel de los

subárboles que no contiene el valor buscado (valores todos inferiores o todos superiores).

45

50

52

9

10

11

12

5

7

13

48

El algoritmo de búsqueda del elemento —clave x— se realiza comparándolo con la clave del raíz del árbol. Si

no es el mismo, se pasa al subárbol izquierdo o derecho, según el resultado de la comparación, y se repite la búsqueda en ese subárbol. La terminación del procedimiento se producirá cuando:

• Se encuentra la clave.

• No se encuentra la clave; se continúa hasta encontrar un subárbol vacío.

procedimiento buscar (E punt: RAIZ;

E <tipo_elemento>: elemento;

S punt: actual, anterior)

var

logico: encontrado

inicio

encontrado ← falso

anterior ← nulo

actual ← raiz

mientras no encontrado Y (actual<>nulo) hacer

si actual→.elemento = elemento entonces

P:94

498 Fundamentos de programación

encontrado ← verdad

si_no

anterior ← actual

si actual→.elemento > elemento entonces

actual ← actual→.izdo

si_no

actual ← actual→.dcho

fin_si

fin_si

fin_mientras

si no encontrado entonces

escribir('no existe', elemento)

si_no

escribir( elemento, 'existe')

fin_si

fin_procedimiento

//< tipo_elemento> en este algoritmo es un tipo de dato simple

13.4.2. Insertar un elemento

Para insertar un elemento en el árbol A se ha de comprobar, en primer lugar, que el elemento no se encuentra en el

árbol, ya que su caso no precisa ser insertado. Si el elemento no existe, la inserción se realiza en un nodo en el que

al menos uno de los dos punteros izq o der tenga valor nulo.

Para realizar la condición anterior se desciende en el árbol a partir del nodo raíz, dirigiéndose de izquierda a derecha de un nodo, según que el valor a insertar sea inferior o superior al valor del campo clave INFO de este nodo.

Cuando se alcanza un nodo del árbol en que no se puede continuar, el nuevo elemento se engancha a la izquierda o

derecha de este nodo en función de que su valor sea inferior o superior al del nodo alcanzado.

El algoritmo de inserción del elemento x es:

procedimiento insertar (E/S punt: raiz;

E <tipo_elemento> : elemento)

var

punt : nuevo,

actual,

anterior

inicio

buscar (raiz, elemento, actual, anterior)

si actual<> NULO entonces

escribir ('elemento duplicado')

si_no

reservar (nuevo)

nuevo→.elemento ← elemento

nuevo→.izdo ← nulo

nuevo→.dcho ← nulo

si anterior = nulo entonces

raiz ← nuevo

si_no

si anterior→.elemento > elemento entonces

anterior→.izdo ← nuevo

si_no

anterior→.dcho ← nuevo

fin_si

fin_si

fin_si

fin_procedimiento

// <tipo_elemento> es un tipo simple

P:95

Estructura de datos no lineales (árboles y grafos) 499

–7

4

49

T

(a)

100

19 –7

49

T

(b)

100

19

(c)

19

0

22

49

100

12

4

0

T

4

–7

Figura 13.18. Inserciones en un árbol de búsqueda binaria: (a) insertar 100, (b) insertar (0), (c) insertar 22 y 12.

Para insertar el valor x en el árbol binario ordenado se necesitará llamar al subprograma insertar.

13.4.3. Eliminación de un elemento

La eliminación de un elemento debe conservar el orden de los elementos del árbol. Se consideran diferentes casos,

según la posición del elemento o nodo en el árbol:

• Si el elemento es una hoja, se suprime simplemente.

• Si el elemento no tiene más que un descendiente, se sustituye entonces por ese descendiente.

• Si el elemento tiene dos descendientes, se sustituye por el elemento inmediato inferior situado lo más a la derecha posible de su subárbol izquierdo.

Para poder realizar estas acciones será preciso conocer la siguiente información del nodo a eliminar:

• Conocer su posición en el árbol.

• Conocer la dirección de su padre.

• Conocer si el nodo a eliminar tiene hijos, si son 1 o 2 hijos, y en el caso de que sólo sea uno, si es hijo derecho

o izquierdo.

La Figura 13.19 muestra los tres posibles casos de eliminación de un nodo: (a) eliminar C, (b) eliminar F, (c)

eliminar B.

(a)

T

A \\

B

(b)

T

A F

B

C E

D

(c)

T

E

D

A C

B

C

Figura 13.19. Casos posibles de eliminación de un nodo.

P:96

500 Fundamentos de programación

En la Figura 13.20 se muestra el caso de eliminación de un nodo con un subárbol en un gráfico comparativo

antes y después de la eliminación.

(a)

19

4

–7

49

100

T

N

(b)

19

4

–7

100

T

Figura 13.20. Eliminación de un nodo con un subárbol.

En la Figura 13.21 se muestra el caso de la eliminación de un nodo (27) que tiene dos subárboles no nulos. En

este caso se busca el nodo sucesor cuyo campo de información le siga en orden ascendente, es decir, 42, se intercambia entonces con el elemento que se desea borrar, 27.

(a)

27

4

–7

22

12

49

100

T

N

42

(b)

42

4

–9

–7

22

12

49

100

T

–9

Figura 13.21. Eliminación de un nodo con dos subárboles no nulos.

EJEMPLO 13.8

Deducir los árboles resultantes de eliminar el elemento 3 en el árbol A y el elemento 7 en el árbol B.

Árbol A

9

10

11

19

3

4

5

6

2

8

17

9

10

11

19

4

5

6

2

8

17

P:97

Estructura de datos no lineales (árboles y grafos) 501

Árbol B. En este caso se busca el nodo sucesor cuyo campo de información le siga en orden decreciente, es decir, 6.

18

24

7

8

9

10

16

2

4

6

18

24

6

8

9

10

16

2

4

Árbol binario mediante arrays

Los árboles deben ser tratados como estructuras dinámicas. No obstante, si el lenguaje no tiene punteros podremos

simularlos mediante arrays.

ELEMENTO IZQ DER

N.o

X X X X N.o N. o

RAÍZ

N.o

VACÍO

Izdo y dcho serán dos campos numéricos para indicar la posición en que están los hijos izquierdo y derecho. El

valor 0 indicaría que no tiene hijo.

Trataremos el array como una lista ENLAZADA y necesitaremos una LISTA DE VACIOS y una variable VACIO

que apunte al primer elemento de la lista de vacíos. Para almacenar la lista de vacíos es indiferente que utilicemos el

campo Izdo o Dcho.

EJEMPLO 13.9

algoritmo arbol_binario_mediante_arrays

const

Max = <expresion>

tipo

registro: TipoElemento

... : ...

... : ...

fin_registro

registro: TipoNodo

TipoElemento : Elemento

Entero : Izdo, Dcho

fin_registro

array[1..Max] de TipoNodo : Arr

var

Arr : a

P:98

502 Fundamentos de programación

Entero : opcion

TipoElemento : elemento

Entero : raiz

Entero : vacio

inicio

iniciar(a,raiz,vacio)

repetir

menu

escribir ('OPCIÓN: ')

repetir

leer (opcion)

hasta_que (opcion >= 0) Y (opcion <= 3)

según_sea opcion hacer

1 :

listado (a,raiz)

escribir ('INTRODUZCA NUEVO ELEMENTO: ')

proc_leer (elemento)

altas (a, elemento, raiz, vacio)

listado (a,raiz)

pausa

2 :

listado (a,raiz)

escribir ('INTRODUZCA ELEMENTO A DAR DE BAJA: ')

proc_leer (elemento)

bajas (a, elemento, raiz, vacio)

listado (a,raiz)

pausa

3:

listado (a,raiz)

pausa

fin_según

hasta_que opcion = 0

fin

procedimiento pausa

var

cadena : c

inicio

escribir('PULSE RETURN PARA CONTINUAR')

leer(c)

fin_procedimiento

procedimiento menu

inicio

escribir ('1.- ALTAS')

escribir ('2.- BAJAS')

escribir ('3.- LISTADO')

escribir ('0.- FIN')

fin_procedimiento

procedimiento iniciar(S Arr: a; S Entero: raiz, vacio)

var

Entero: i

inicio

raiz ← 0

P:99

Estructura de datos no lineales (árboles y grafos) 503

vacio ← 1

desde i ← 1 hasta Max-1 hacer

a[i].dcho ← i+1

fin_desde

a[Max].dcho ← 0

fin_procedimiento

logico función ArbolVacio(E Entero: raiz)

inicio

si raiz=0 entonces

devolver(verdad)

si_no

devolver(falso)

fin_si

fin_función

logico funcion ArbolLleno(E Entero: vacio)

inicio

si vacio=0 entonces

devolver(verdad)

si_no

devolver(falso)

fin_si

fin_función

procedimiento inorden(E/S Arr: a; E Entero: raiz)

inicio

si raiz <> 0 entonces

inorden(a,a[raiz].Izdo)

proc_escribir(a[raiz].elemento)

inorden(a,a[raiz].Dcho)

fin_si

fin_procedimiento

procedimiento preorden(E/S Arr: a; E Entero: raiz)

inicio

si raiz <> 0 entonces

proc_escribir(a[raiz].elemento)

preorden(a,a[raiz].Izdo)

preorden(a,a[raiz].Dcho)

fin_si

fin_procedimiento

procedimiento postorden(E/S Arr: a; E Entero: raiz)

inicio

si raiz <> 0 entonces

postorden(a,a[raiz].Izdo)

postorden(a,a[raiz].Dcho)

proc_escribir(a[raiz].elemento)

fin_si

fin_procedimiento

procedimiento buscar(E/S Arr: a; E entero: raiz;

E TipoElemento: elemento;

S entero: act, ant)

var

logico: encontrado

P:100

504 Fundamentos de programación

inicio

encontrado ← falso

act ← raiz

ant ← 0

mientras no encontrado y (act<>0) hacer

si igual(elemento, a[act].elemento) entonces

encontrado ← verdad

si_no

ant ← act

si mayor(a[act].elemento, elemento) entonces

act ← a[act].Izdo

si_no

act ← a[act].Dcho

fin_si

fin_si

fin_mientras

fin_procedimiento

procedimiento altas(E/S Arr: A; E TipoElemento: elemento;

E/S entero: raiz, vacio)

var

entero: act,ant,auxi

inicio

si vacio <> 0 entonces

buscar(a, raiz, elemento, act, ant)

si act <> 0 entonces

escribir('ESE ELEMENTO YA EXISTE')

si_no

auxi ← vacio

vacio ← a[auxi].Dcho

a[auxi].elemento ← elemento

a[auxi].Izdo ← 0

a[auxi].Dcho ← 0

si ant = 0 entonces

raiz ← auxi

si_no

si mayor(a[ant].elemento, elemento) entonces

a[ant].Izdo ← auxi

si_no

a[ant].Dcho ← auxi

fin_si

fin_si

fin_si

fin_si

fin_procedimiento

procedimiento bajas(E/S Arr: A; E TipoElemento: elemento;

E/S entero: raiz, vacio)

var

entero: act, ant, auxi

inicio

buscar(a, raiz, elemento, act, ant)

si act = 0 entonces

escribir('ESE ELEMENTO NO EXISTE')

si_no

si (a[act].Izdo = 0) Y (a[act].Dcho = 0) entonces

P:101

Estructura de datos no lineales (árboles y grafos) 505

si ant = 0 entonces

raiz ← 0

si_no

si a[ant].Izdo = act entonces

a[ant].Izdo ← 0

si_no

a[ant].Dcho ← 0

fin_si

fin_si

si_no

si (a[act].Izdo <> 0) Y (a[act].Dcho <> 0) entonces

ant ← act

auxi ← a[act].Izdo

mientras a[auxi].Dcho <> 0 hacer

ant ← auxi

auxi ← a[auxi].Dcho

fin_mientras

a[act].Elemento ← a[auxi].Elemento

si ant = act entonces

a[ant].Izdo ← a[auxi].Izdo

si_no

a[ant].Dcho ← a[auxi].Izdo

fin_si

act ← auxi

si_no

si a[act].Dcho <> 0 entonces

si ant = 0 entonces

raíz ← a[act].Dcho

si_no

si a[ant].Izdo = act entonces

a[ant].Izdo ← a[act].Dcho

si_no

a[ant].Dcho ← a[act].Dcho

fin_si

fin_si

si_no

si ant = 0 entonces

raíz ← a[act].Izdo

si_no

si a[ant].Dcho = act entonces

a[ant].Dcho ← a[act].Izdo

si_no

a[ant].Izdo ← a[act].Izdo

fin_si

fin_si

fin_si

fin_si

fin_si

a[act].Dcho ← vacio

vacio ← act

fin_si

fin_procedimiento

procedimiento listado (E Arr: a; E Entero: raiz)

inicio

escribir ('INORDEN: ')

P:102

506 Fundamentos de programación

inorden (a, raiz)

escribir ('PREORDEN: ')

preorden (a, raiz)

escribir ('POSTORDEN: ')

postorden (a, raiz)

fin_procedimiento

ELEMENTO DCHO

0

RAÍZ

VACÍO

2

3

4

5

6

7

0

1

Array

3

0

0

5

6

7

0

2

0

0

elemento 1

elemento 2

elemento 3

VACÍO

1 4

RAÍZ

3

4

0

5

6

7

0

0

0

0

elemento 1

elemento 2

elemento 3

VACÍO

1 2

RAÍZ

Iniciar Iniciar

13.5. GRAFOS

Los grafos son otra estructura de datos no lineal y que tiene gran número de aplicaciones. El estudio del análisis de

grafos ha interesado a los matemáticos durante siglos y representa una parte importante de la teoría combinatoria en

matemáticas. Aunque la teoría de grafos es compleja y amplia, en esta sección se realizará una introducción a la

teoría de grafos y a los algoritmos que permiten su solución por computadora.

Los árboles binarios representan estructuras jerárquicas con limitaciones de dos subárboles por cada nodo. Si se

eliminan las restricciones de que cada nodo puede apuntar a dos nodos —como máximo— y que cada nodo puede

estar apuntado por otro nodo —como máximo— nos encontramos con un grafo.

Ejemplos de grafos en la vida real los tenemos en la red de carreteras de un estado o región, la red de enlaces

ferroviarios o aéreos nacionales, etc.

En una red de carreteras los nudos de la red representan los vértices del grafo y las carreteras de unión de dos

ciudades los arcos, de modo que a cada arco se asocia una información tal como la distancia, el consumo en gasolina por automóvil, etc.

Los grafos nos pueden ayudar a resolver problemas como éste. Supóngase que ciertas carreteras del norte del

Estado han sido bloqueadas por una reciente tormenta de nieve. ¿Cómo se puede saber si todas las ciudades de ese

Estado se pueden alcanzar por carretera desde la capital o si existen ciudades aisladas? Evidentemente existe la solución del estudio de un mapa de carreteras; sin embargo, si existen muchas ciudades, la obtención de la solución

puede ser ardua y costosa en tiempo. Una computadora y un algoritmo adecuado de grafos solucionarán fácilmente

el problema.

13.5.1. Terminología de grafos

Formalmente un grafo es un conjunto de puntos —una estructura de datos— y un conjunto de líneas, cada una de

las cuales une un punto a otro. Los puntos se llaman nodos o vértices del grafo y las líneas se llaman aristas o arcos

(edges).

Se representan el conjunto de vértices de un grafo dado G por VG y el conjunto de arcos por AG. Por ejemplo, en

el grafo G de la Figura 13.23:

VG = {a, b, c, d}

AG = {1, 2, 3, 4, 5, 6, 7, 8}

P:103

Estructura de datos no lineales (árboles y grafos) 507

PARÍS

NANCY

MARSELLA TOULOUSE

BURDEOS

NANTES

LYON

386

491

468 557

562

383

338

224

485

393

314

Figura 13.22. Grafo de una red de carreteras.

b

1

a 8 7

6

5

3

2

c

4

d

Figura 13.23. Grafo G.

El número de elementos de VG se llama orden del grafo. Un grafo nulo es un grafo de orden cero.

Una arista se representa por los vértices que conecta. La arista 3 conecta los vértices b y c, y se representa por

V(b, c). Algunos vértices pueden conectar un nodo consigo mismo; por ejemplo, la arista 8 tiene el formato V(a, a).

Estas aristas se denominan bucles o lazos.

Un grafo G se denomina sencillo si se cumplen las siguientes condiciones:

• No tiene lazos, no existe un arco en AG de la forma (V, V).

• No existe más que un arco para unir dos nodos, es decir, no existe más que un arco (V1, V2) para cualquier par

de vértices V1, V2.

En la Figura 13.24 se representa un grafo sencillo.

b

1

a

3

2

4 c

d

Figura 13.24. Grafo sencillo.

Un grafo que no es sencillo se denomina grafo múltiple.

Un camino es una secuencia de uno o más arcos que conectan dos nodos. Representaremos por C(Vi

, Vj

) un camino que conecta los vértices Vi

y Vj

.

P:104

508 Fundamentos de programación

La longitud de un camino es el número de arcos que comprende. En el grafo de la Figura 13.24 existen los siguientes caminos entre los nodos b y d.

C(b,d) = (b,c) (c,d) longitud = 2

C(b,d) = (b,c) (c,b) (b,c) (c,d) longitud = 4

C(b,d) = (b,d) longitud = 1

C(b,d) = (b,d) (c,b) (b,d) longitud = 3

Dos vértices se dice que son adyacentes (inmediatos) si hay un arco que los une. Así, Vi

y Vj

son adyacentes si

existe un camino que los une. Esta definición es muy general y normalmente se particulariza; si existe un camino

desde A a B, decimos que A es adyacente a B y B es adyacente desde A. Así, en el grafo de la Figura 13.25, Las

Vegas es adyacente a Nueva York, pero Nueva York no es adyacente a Las Vegas.

Se consideran dos tipos de grafos:

Dirigidos los vértices apuntan unos a otros; los arcos están dirigidos o tienen dirección.

No-dirigidos los vértices están relacionados, pero no se apuntan unos a otros; la dirección no es importante.

En la Figura 13.25 el grafo es dirigido, dado que la dirección es importante; así, existe un vuelo entre Las Vegas

y Nueva York, pero no en sentido contrario.

grafo conectado existe siempre un camino que une dos vértices cualesquiera,

grafo desconectado existen vértices que no están unidos por un camino.

1

3

2

4 5

6

1

3

2

4 5

6

(a) no dirigido (b) dirigido

Figura 13.25. Grafo: (a) no dirigido, (b) dirigido.

F

A

H J

G

(a) conectado

A

C

F

E

G

(b) conectado (b) no conectado

B

C

D

I

E

B

D

H

A

C

E F

G

B

D

H

J

L

I

K

Figura 13.26. Grafos conectados y no conectados.

Otros tipos de grafos de gran interés se muestran en la Figura 13.27. Un grafo completo es aquel en que cada

vértice está conectado con todos y cada uno de los restantes nodos. Si existen n vértices, habrá n(n – 1) arcos en un

grafo completo y dirigido, y n(n – 1)/2 aristas en un grafo no dirigido completo.

A B

C D

(a) grafo completo dirigido

J

K N

L M

(b) grafo completo no dirigido

Figura 13.27. Grafos completos.

P:105

Estructura de datos no lineales (árboles y grafos) 509

Un grafo ponderado o con peso es aquel en el que cada arista o arco tiene un valor. Los grafos con peso suelen

ser muy importantes, ya que pueden representar situaciones de gran interés; por ejemplo, los vértices pueden ser

ciudades y las aristas distancias o precios del pasaje de ferrocarril o avión entre ambas ciudades. Eso nos puede permitir calcular cuál es el recorrido más económico entre dos ciudades, sumando los importes de los billetes de las

ciudades existentes en el camino y así poder tomar una decisión acertada respecto al viaje e incluso estudiar el posible cambio de medio de transporte: avión o automóvil, si éstos resultan más baratos.

La solución de encontrar el camino más corto, el de menor precio o más económico entre dos vértices de un

grafo, es un algoritmo importante en la teoría de grafos. (El algoritmo de Dijkstra es un algoritmo tipo para la solución de dichos problemas.)

13.5.2. Representación de grafos

Existen dos técnicas estándar para representar un grafo G: la matriz de adyacencia (mediante arrays) y la lista de

adyacencia (mediante punteros/listas enlazadas).

13.5.2.1. Matriz de adyacencia

La matriz de adyacencia M es un array de dos dimensiones que representa las conexiones entre pares de vértices. Sea

un grafo G con un conjunto de nodos VG y un conjunto de aristas AG. Supongamos que el grafo es de orden N, donde

N >= 1. La matriz de adyacencia M se representa por una matriz de N × N elementos, donde:

M(i, j) = { 1 si existe un arco (Vi

, Vj

) en AG, Vi

es adyacente a Vj

0, en caso contrario

Las columnas y las filas de la matriz representan los vértices del grafo. Si existe una arista desde i a j (esto es, el

vértice i es adyacente a j), se introduce un 1; si no existe la arista, se introduce un 0; lógicamente, los elementos de

la diagonal principal son todos ceros, ya que el coste de la arista i a i es 0.

Si G es un grafo no dirigido, la matriz es simétrica M(i, j) = M(j, i). La matriz de adyacencia del grafo de la Figura 13.25 se indica en la Figura 13.28.

j

i 123456

10 1 0 0 0 0

21 0 1 0 0 0

30 1 0 1 1 1

40 0 1 0 0 0

50 0 1 0 0 0

60 0 1 0 0 0

Figura 13.28. Matriz de adyacencia.

Si el grafo fuese dirigido, su matriz resultante sería:

j

i 123456

10 1 0 0 0 0

20 0 1 0 0 0

30 0 0 0 1 1

40 0 1 0 0 0

50 0 0 0 0 0

60 0 0 0 0 0

P:106

510 Fundamentos de programación

EJEMPLO 13.10

Deducir la matriz de adyacencia del grafo siguiente:

La matriz de adyacencia resultante de este grafo, cuyos vértices representan ciudades y los pesos de las aristas,

los precios de pasajes de avión en dólares es

Solución

3000 3000

390

2000

2000

1000

1500

1000

3500

LA LV

SF

NY

KC

SF LA LV KC NY

SF 1000

LA 1000 390 2000

LV 390 3000 2500

KC 2000 3000

NY 1500 350

EJEMPLO 13.11

Sea un grafo con aristas ponderadas. Los vértices representan ciudades y las aristas las rutas utilizadas por los camiones de una empresa de transporte de mercancías. Cada arista está rotulada con la distancia entre las parejas de

ciudades enlazadas directamente. En este caso utilizaremos una matriz triangular, ya que la matriz es simétrica.

Nota

Obsérvese como consecuencia de este ejemplo que las aristas ponderadas tienen una gran aplicación.

• En transporte comúnmente representan distancias, precios de billetes, tiempos.

• En hidráulica, capacidades. Por ejemplo, el caudal de un oleoducto entre diferentes ciudades litros/segundo.

13.5.2.2. Lista de adyacencia

El segundo método utilizado para representar grafos es útil cuando un grafo tiene muchos vértices y pocas aristas;

es la lista de adyacencia. En esta representación se utiliza una lista enlazada por cada vértice v del grafo que tenga

vértices adyacentes desde él.

El grafo completo incluye dos partes: un directorio y un conjunto de listas enlazadas. Hay una entrada en el directorio por cada nodo del grafo. La entrada en el directorio del nodo i apunta a una lista enlazada que representa los

nodos que son conectados al nodo i. Cada registro de la lista enlazada tiene dos campos: uno es un identificador de

nodo, otro es un enlace al siguiente elemento de la lista; la lista enlazada representa arcos.

Una lista de adyacencia del grafo de la Figura 13.25a se da en la Figura 13.29.

6 NULO

3

5

2 NULO

4

2 NULO

1

2

3

3

3

NULO

NULO

NULO

1

2

3

4

5

6

DIRECTORIO

Figura 13.29. Lista de adyacencia.

P:107

Estructura de datos no lineales (árboles y grafos) 511

Un grafo no dirigido de orden N con A arcos requiere N entradas en el directorio y 2*A entradas de listas enlazadas, excepto si existen bucles que reducen el número de listas enlazadas en 1.

Un grafo dirigido de orden N con A arcos requiere N entradas en el directorio y A entradas de listas enlazadas.

3

6

2 NULO

2

5

3 NULO

1

2

3

4

5

6

NULO

NULO

NULO

NULO

DIRECTORIO

Figura 13.30.

EJEMPLO 13.12

La lista de adyacencia del grafo del Ejemplo 13.10 es

SF

LA

LV

KC

NY

LA 1000

SF 1000

LA 390

LA 2000

SF 1500

LV 390

KC 3000

LV 3000

KC 3500

KC 2000

NY 2500

La elección de la representación depende del algoritmo particular que se vaya a implementar y si el grafo es

“disperso” o “denso”. Un grafo disperso es uno en el que el número de vértices N es mucho mayor que el número

de arcos. En un grafo denso el número de arcos se acerca al máximo.

P:108

512 Fundamentos de programación

ACTIVIDADES DE PROGRAMACIÓN RESUELTAS

13.1. Deducir las fórmulas de las expresiones representadas por los siguientes árboles de expresión.

!

x n

+ log

a b

a *

b c

o

< <

a b c d

a)

d)

b) c)

e)

a) a + b

b) log x

c) n!

d) a – (b * c)

e) (a < b) o (c < d)

13.2. Deducir la fórmula que representa el siguiente árbol expresión.

– /

1 2

+

– ^

X /

*

2 a

b

^ *

b 2 * c

4 a

x = (–b + (b2

– 4 * a * c)

(1/2)) / (2 * a)

es decir, una de las raíces solución de la ecuación cuadrática o de segundo grado:

ax2

+ bx + c = 0.

13.3. Teniendo en cuenta que nuestro lenguaje de programación no maneja estructuras dinámicas de datos, escribir un

procedimiento que inserte un nuevo nodo en un árbol binario en el lugar correspondiente según su valor. Escribir

otro procedimiento que permita conocer el número de nodos de un árbol binario. Utilizar ambos procedimientos

desde un algoritmo que cree el árbol y nos informe sobre su número de nodos.

P:109

Estructura de datos no lineales (árboles y grafos) 513

Análisis del problema

El procedimiento de inserción será análogo al de altas que aparece en al ejercicio 13.9. Para conocer el número de nodos

del árbol se realiza su recorrido, por uno cualquiera de los métodos ya comentados —inorden, preorden, postorden— y se

irán contando.

El programa principal comenzará con un proceso de inicialización, a continuación utilizará una estructura repetitiva

que permita la inserción de un número indeterminado de nodos en el árbol y, por último, llamará al procedimiento para

contarlos.

Al no especificarse en el enunciado el tipo de información que se almacena en los registros del árbol, ésta se tratará de

forma genérica, recurriendo a procedimientos y funciones auxiliares no desarrollados que permitan manipularla, como, por

ejemplo, leerelemento(elemento), escribirelemento(elemento), distinto(elemento,'0').

Diseño del algoritmo

algoritmo ejercicio_13_3

const

Máx = ...

tipo

registro: tipoelemento

... : ...

... : ...

fin_registro

registro: tiponodo

tipoelemento : elemento

entero : izdo, dcho

fin_registro

array[1..Máx] de tiponodo: arr

var

arr : a

tipoelemento : elemento

entero : raíz, vacío

inicio

iniciar(a,raíz,vacío)

escribir ('Introduzca nuevo elemento: ')

si no árbollleno(vacío) entonces

leerelemento (elemento)

fin_si

mientras distinto(elemento,'0') y no árbollleno(vacío) hacer

altas (a, elemento, raíz, vacío)

si no árbollleno(vacío) entonces

escribir ('Introduzca nuevo elemento: ')

leerelemento (elemento)

fin_si

fin_mientras

listado (a, raíz)

fin

procedimiento iniciar(S arr: a ; S entero: raíz, vacío)

var

entero: i

inicio

raíz ← 0

vacío ← 1

desde i ← 1 hasta Máx-1 hacer

a[i].dcho ← i+1

fin_desde

a[Máx].dcho ← 0

fin_procedimiento

P:110

514 Fundamentos de programación

lógico función árbollleno(E entero: vacío)

inicio

si vacío = 0 entonces

devolver(verdad)

si_no

devolver(falso)

fin_si

fin_función

procedimiento inorden(E arr: a; E entero: raíz; E/S entero: cont)

inicio

si raíz <> 0 entonces

inorden(a, a[raíz].izdo, cont)

cont ← cont + 1

escribirelemento(a[raíz].elemento)

// Además de contar los nodos visualiza la

// información almacenada en ellos

inorden(a, a[raíz].dcho, cont)

fin_si

fin_procedimiento

procedimiento buscar(E arr: a ; E tipoelemento:elemento;

S entero: act, ant)

var

lógico: encontrado

inicio

encontrado ← falso

act ← raíz

ant ← 0

mientras no encontrado y (act <> 0) hacer

si igual(elemento, a[act].elemento) entonces

encontrado ← verdad

si_no

ant ← act

si mayor(a[act].elemento, elemento) entonces

act ← a[act].izdo

si_no

act ← a[act].dcho

fin_si

fin_si

fin_mientras

fin_procedimiento

procedimiento altas(E/S arr: a; E tipoelemento: elemento;

E/S entero: raíz, vacío)

var

entero: act, ant, auxi

inicio

si no árbollleno(vacío) entonces

buscar(a, elemento, act, ant)

si act <> 0 entonces

escribir('Ese elemento ya existe')

si_no

auxi ← vacío

vacío ← a[auxi].dcho

a[auxi].elemento ← elemento

a[auxi].izdo ← 0

a[auxi].dcho ← 0

si ant = 0 entonces

P:111

Estructura de datos no lineales (árboles y grafos) 515

raíz ← auxi

si_no

si mayor(a[ant].elemento, elemento) entonces

a[ant].izdo ← auxi

si_no

a[ant].dcho ← auxi

fin_si

fin_si

fin_si

fin_si

fin_procedimiento

procedimiento listado (E arr: a; E entero: raíz)

var

entero: cont

inicio

escribir ('inorden: ')

cont ← 0

inorden (a, raíz, cont)

escribir('El número de nodos es ', cont)

fin_procedimiento

13.4. Escribir un procedimiento que permita contar las hojas de un árbol mediante estructuras dinámicas.

Análisis del problema

Recorrer el árbol, contando, únicamente, los nodos que no tienen hijos.

Diseño del algoritmo

procedimiento contarhojas(E punt: raíz; E/S entero: cont)

inicio

si raíz <> nulo entonces

si (raíz→.izdo = nulo) y (raíz→.dcho = nulo) entonces

cont ← cont+1

fin_si

contarhojas(raíz→.izdo, cont)

contarhojas(raíz→.dcho, cont)

fin_si

fin_procedimiento

13.5. Diseñar una función que permita comprobar si son iguales dos árboles cuyos nodos tienen la siguiente estructura:

tipo

puntero_a nodo: punt

registro : nodo

entero : elemento

punt : izdo, dcho

fin_registro

Análisis del problema

Se trata de una función recursiva que compara nodo a nodo la información almacenada en ambos árboles. Las condiciones

de salida del proceso recursivo serán que:

• Se termine de recorrer uno de los dos árboles.

• Se terminen de recorrer ambos.

• Se encuentre diferente información en los nodos comparados.

P:112

516 Fundamentos de programación

Si los árboles terminaron de recorrerse simultáneamente es que ambos tienen el mismo número de nodos y nunca ha

sido diferente la información comparada; por tanto, la función devolverá verdad; en cualquier otro caso la función devolverá

falso.

Diseño del algoritmo

lógico función iguales(E punt: raíz1, raíz2)

inicio

si raíz1 = nulo entonces

si raíz2 = nulo entonces

devolver(verdad)

fin_si

si_no

si raíz2 = nulo entonces

devolver(falso)

si_no

si raíz1→.elemento <> raíz2→.elemento entonces

devolver(falso)

si_no

devolver(iguales(raíz1→.izdo, raíz2→.izdo)

y iguales(raíz1→.dcho, raíz2→.dcho))

fin_si

fin_si

fin_si

fin_función

CONCEPTOS CLAVE

• Árbol.

• Árbol binario.

• Árbol binario de búsqueda.

• Dígrafo.

• Enorden.

• Grafo.

• Grafo dirigido.

• Grafo no dirigido.

• Hoja.

• Lista de adyacencia.

• Matriz de adyacencia.

• Nivel.

• Nodo.

• Postorden.

• Preorden.

• Profundidad.

• Raíz.

• Rama.

• Recorrido de un árbol.

• Subárbol.

RESUMEN

Las estructuras de datos dinámicas árboles y grafos son

muy potentes para la resolución de problemas complejos de

tipo gráfico, jerárquico o en red.

La estructura árbol más utilizada normalmente es el árbol binario. Un árbol binario es un árbol en el que cada

nodo tiene como máximo dos hijos, llamados subárbol izquierdo y subárbol derecho.

En un árbol binario, cada elemento tiene cero, uno o dos

hijos. El nodo raíz no tiene un padre pero sí cada elemento

restante. Cuando un elemento y tiene un padre x, x es un

antecesor o antecedente del elemento y.

La altura de un árbol binario es el número de ramas entre

el raíz y la hoja más lejana más 1. Si el árbol A es vacío.

El nivel o profundidad de un elemento es un concepto

similar al de altura.

Un árbol binario no vacío está equilibrado totalmente si

sus subárboles izquierdo y derecho tienen la misma altura

y ambos son o bien vacíos o totalmente equilibrados.

Los árboles binarios presentan dos tipos característicos:

árboles binarios de búsqueda y árboles binarios de expresiones. Los árboles binarios de búsqueda se utilizan fundamentalmente para mantener una colección ordenada de

datos y los árboles binarios de expresiones para almacenar

expresiones.

Los grafos son otra estructura de datos no lineal y que

tiene gran número de aplicaciones. Los árboles binarios re-

P:113

Estructura de datos no lineales (árboles y grafos) 517

presentan estructuras jerárquicas con limitaciones de dos subárboles por cada nodo. Si se eliminan las restricciones de

que cada nodo puede apuntar a dos nodos —como máximo— y que cada nodo puede estar apuntado por otro nodo

—como máximo— nos encontramos con un grafo. Ejemplos de grafos en la vida real los tenemos en la red de carreteras de un estado o región, la red de enlaces ferroviarios

o aéreos nacionales, etc.

Un grafo G consta de dos conjuntos (G = {V, E}):

un conjunto V de vértices o nodos y un conjunto E de aristas (parejas de vértices distintos) que conectan los vértices.

Si las parejas no están ordenadas, G se denomina grafo no

dirigido; si los pares están ordenados, entonces G se denomina grafo dirigido. El término grafo dirigido se suele también designar como dígrafo y el término grafo sin calificación significa grafo no dirigido.

Los grafos se pueden implementar de dos formas típicas: matriz de adyacencia y lista de adyacencia. La elección depende de las necesidades de la aplicación en concreto, ya que cada una de las formas tiene sus ventajas y sus

inconvenientes.

El recorrido de un grafo puede ser en analogía con los

árboles, recorrido en profundidad y recorrido en anchura.

El recorrido en profundidad es aplicable a los grafos dirigidos y a los no dirigidos y es una generalización del recorrido preorden de un árbol. El recorrido en anchura también

es aplicable a grafos dirigidos y no dirigidos, que generaliza el concepto de recorrido por niveles de un árbol.

EJERCICIOS

13.1. Dado un árbol binario de números enteros ordenados, se desea un subalgoritmo que busque un elemento con un proceso recursivo.

13.2. Diseñar un subalgoritmo que busque un elemento en

un árbol binario de números enteros ordenados, realizado con un proceso repetitivo.

13.3. Describir el orden en el que los vértices de los siguientes árboles binarios serán visitados en: a) preorden, b) in-orden, c) post-orden:

(a) 1

2

3

4

5

(b) 1

2

3

4

5

T

(c) 1

2 23

3

5 6

7 8

(d) 1

4 6

7 89

4 5

13.4. Dibujar la expresión árbol para cada una de las siguientes expresiones y dar el orden de visita a los

nodos en: a) pre-orden, b) in-orden, c) post-orden:

1. log n!

2. (a – b) – c

3. a – (b – c)

4. (a < b) y (b < c) y (c < d)

13.5. Escribir un subalgoritmo recursivo que liste los

nodos de un árbol binario en pre-orden.

13.6. Escribir un subalgoritmo que elimine un nodo determinado de un árbol de enteros.

13.7. Se dispone de un árbol de números reales desordenados y se desea escribir un subalgoritmo que inserte un nodo en el lugar correspondiente de acuerdo a

su valor.

13.8. Escribir un subalgoritmo que permita conocer el número de nodos de un árbol binario.

P:114

518 Fundamentos de programación

13.9. Considerar el árbol binario.

Listar los nodos del árbol en: a) pre-orden, b) inorden, c) post-orden.

B C

A

D

G

M

Q

L

E

H I

F

J K

N O

P:115

CAPÍTULO 14

Recursividad

14.1. La naturaleza de la recursividad

14.2. Recursividad directa e indirecta

14.3. Recursión versus iteración

14.4. Recursión infinita

14.5. Resolución de problemas complejos con

recursividad

CONCEPTOS CLAVE

RESUMEN

EJERCICIOS

PROBLEMAS

La recursividad (recursión) es aquella propiedad que

posee una función por la cual dicha función puede

llamarse a sí misma. Se puede utilizar la recursividad

como una alternativa a la iteración. Una solución recursiva es normalmente menos eficiente en términos

de tiempo de computadora que una solución iterativa

debido a las operaciones auxiliares que llevan consigo

las llamadas suplementarias a las funciones; sin embargo, en muchas circunstancias el uso de la recursión

permite a los programadores especificar soluciones

naturales, sencillas, que serían, en caso contrario, difíciles de resolver. Por esta causa, la recursión es una

herramienta poderosa e importante en la resolución

de problemas y en la programación.

INTRODUCCIÓN

P:116

520 Fundamentos de programación

14.1. LA NATURALEZA DE LA RECURSIVIDAD1

Los programas examinados hasta ahora, generalmente estructurados, se componen de una serie de funciones que

llaman unas a otras de modo disciplinado. En algunos problemas es útil disponer de funciones que se llamen a sí

misma. Un subprograma recursivo es un subprograma que se llama a sí mismo ya sea directa o indirectamente. La

recursividad es un tópico importante examinado frecuentemente en cursos de programación y de introducción a las

ciencias de la computación.

En este libro se dará una importancia especial a las ideas conceptuales que soportan la recursividad. En matemáticas existen numerosas funciones que tienen carácter recursivo; de igual modo numerosas circunstancias y situaciones de la vida ordinaria tienen carácter recursivo.

Hasta el momento casi siempre se han visto subprogramas que llaman a otros subprogramas distintos. Así, si se

dispone de dos procedimientos proc1 y proc2, la organización de un programa tal y como se suele haber visto

hasta este momento podría adoptar una forma similar a esta:

procedimiento proc1(...)

inicio

...

fin_procedimiento

procedimiento proc2(...)

inicio

...

proc1(...) // llamada a proc1

...

fin_procedimiento

Cuando diseñan programas recursivos se tendría esta situación:

procedimiento proc1(...)

inicio

...

proc1(...);

...

fin_procedimiento

o bien esta otra:

procedimiento proc1(...)

inicio

...

proc2(...) //llamada a proc2

...

fin_procedimiento

procedimiento proc2(...)

inicio

...

proc1(...) //llamada a proc1

...

fin_procedimiento

1

Las palabras inglesas «recursive» y «recursion» no han sido aceptadas todavía por el Diccionario de la Real Academia de la Lengua Española (www.rae.es). La última edición (22.ª, Madrid, 2001) editada conjuntamente por todas las Academias de la Lengua de España, Latinoamérica y Estados Unidos, recoge sólo los términos sinónimos siguientes en sus acepciones Mat (Matemáticas): recurrencia, «propiedad de aquellas

secuencias en las que cualquier término se puede calcular conociendo los precedentes», y recurrente, «Dicho de un proceso: que se repita».

P:117

Recursividad 521

EJEMPLO 14.1

El factorial de un entero no negativo n, escrito n! (y pronunciado n factorial), es el producto

n! = n.(n – 1).(n – 2),...,1

en el cual

0! = 1

1! = 1

2! = 2*1 = 2*1!

3! = 3*2*1 = 3*2!

4¡ = 4*3*2*1 = 4*3!

...

así:

5! = 5 . 4 . 3 . 2 . 1 = 5 . 4! = 120

de modo que una definición recursiva de la función factorial n es:

n! = n*(n – 1)! para n>1

El factorial de un entero n, mayor o igual a 0, se puede calcular de modo iterativo (no recursivo), teniendo presente la definición de n! del modo siguiente:

n! = 1 si n = 0

n! = n * (n – 1)! si n > 0

El algoritmo que resuelve el factorial de forma iterativa de un entero n, mayor o igual que 0, se puede calcular

utilizando un bucle for:

var

entero: contador

real: factorial

inicio

...

factorial ← 1;

desde contador ← n hasta 1 decremento 1

factorial ← factorial * contador

fin_desde

fin

En el caso de implementar una función se requerirá una sentencia de retorno que devuelva el valor del factorial,

tal como

devolver(factorial)

El algoritmo que resuelve la función de modo recursivo ha de tener presente una condición de salida. Así, en el

caso del cálculo de 6!, la definición es 6! = 6 × 5! y 5! de acuerdo a la definición es 5 × 4! Este proceso

continúa hasta que 1! = 1 × 0! por definición. El método de definición de una función en términos de sí misma

se llama en matemáticas una definición inductiva y conduce naturalmente a una implementación recursiva. El caso

base de 0! = 1 es esencial dado que se detiene, potencialmente, una cadena de llamadas recursivas. Este caso base

o condición de salida deben fijarse en cada caso de una solución recursiva. El algoritmo que resuelve n! de modo

recursivo se apoya en la definición siguiente:

P:118

522 Fundamentos de programación

n! = 1 si n = 0

n! = n*(n – 1)*(n – 2)*...*1 si n > 0

en consecuencia, el algoritmo mencionado que calcula el factorial será:

si (n = 0) entonces

fac ← 1

si_no

contador = n – 1

fac ← n * fac(contador)

fin_si

Otro pseudocódigo que resuelve la función factorial es:

si n = 1 entonces

fac ← n

si_no

fac ← n*fac(n – 1)

fin_si

Así una función recursiva de factorial es:

entero función factorial(E entero: n)

inicio

si (n = 1) entonces

devolver (1)

si_no

devolver (n * factorial(n – 1))

fin_si

fin_función

Nota

Dado que el valor de un factorial de un número entero aumenta considerablemente a medida que aumenta el

valor de n, es conveniente en el diseño del algoritmo definir el tipo de dato a devolver por la función como un

valor real, al objeto de no tener problema de desbordamiento cuando traduzca a un código fuente en un lenguaje de programación.

EJEMPLO 14.2

Deducir la definición recursiva del producto de números naturales.

El producto a * b, donde a y b son enteros positivos, tiene dos soluciones.

Solución iterativa

a * b = a + a + a + + ... + a _________________________

b veces

Solución recursiva a * b = a si b = 1

a * b = a * (b – 1) + a si b > 1

Así, por ejemplo, 7 × 3 será:

7 * 3 = 7 * 2 + 7 = 7 * 1 + 7 + 7 = 7 + 7 + 7 = 21

P:119

Recursividad 523

En pseudocódigo se tiene:

entero funcion producto (E entero, a, b)

inicio

si b = 1 entonces

devolver (a)

si no

devolver (a*producto (a, b-1))

fin_si

fin

EJEMPLO 14.3

Definir la naturaleza de la serie de Fibonacci: 0, 1, 1, 2, 3, 5, 8, 13, 21, ...

Se observa en esta serie que comienza con 0 y 1, y tiene la propiedad de que cada elemento es la suma de los

dos elementos anteriores, por ejemplo:

0 + 1 = 1

1 + 1 = 2

2 + 1 = 3

3 + 2 = 5

5 + 3 = 8

...

Entonces se puede decir que:

fibonacci(0) = 0

fibonacci(1) = 1

...

fibonacci(n) = fibonacci(n – 1) + fibonacci(n – 2)

y la definición recursiva será:

fibonacci(n) = n si n = 0 o n = 1

fibonacci(n) = fibonacci(n – 1) + fibonacci(n – 2) si n > = 2

Obsérvese que la definición recursiva de los números de fibonacci es diferente de las definiciones recursivas del

factorial de un número y del producto de dos números. Así, por ejemplo, simplificando el nombre de la función por

fib

fib(6) = fib(5) + fib(4)

o lo que es igual, fib(6) ha de aplicarse en modo recursivo dos veces, y así sucesivamente. Las funciones iterativa

y recursiva implementadas en Java son

public class Fibonacci

{

//Fibonacci iterativo

public static long fibonaccii(int n)

{

long f = 0, fsig = 1;

for (int i = 0; i < n; i++)

{

long aux = fsig;

P:120

524 Fundamentos de programación

fsig += f;

f = aux:

}

return(f);

}

//Fibonacci recursivo

public static long fibonaccir(int n)

{

// si n es menor que 0 devuelve –1 como señal de error

if (n < 0)

return –1;

// especificar else no es necesario, ya que

// cuando se ejecuta return se retorna a la sentencia llamadora

// y la siguiente instrucción ya no se ejecuta

if (n == 0)

return(0);

else

if (n == 1)

return(1);

else

return(fibonaccir(n–1)+fibonaccir(n–2));

}

public static void main(String[] args)

{

System.out.println("Fibonacci_iterativo("+8+")="+fibonaccii(8));

System.out.println("Fibonacci_recursivo("+8+")="+fibonaccii(8));

}

}

14.2. RECURSIVIDAD DIRECTA E INDIRECTA

En recursión directa el código del subprograma recursivo F contiene una sentencia que invoca a F, mientras que en

recursión indirecta el subprograma F invoca al subprograma G que invoca a su vez al subprograma P, y así sucesivamente hasa que se invoca de nuevo al subprograma F.

Si una función, procedimiento o método se invoca a sí misma, el proceso se denomina recursión directa; si una

función, procedimiento o método puede invocar a una segunda función, procedimiento o método que a su vez

invoca a la primera, este proceso se conoce como recursión indirecta o mutua.

Un requisito para que un algoritmo recursivo sea correcto es que no genere una secuencia infinita de llamadas

sobre sí mismo. Cualquier algoritmo que genere una secuencia de este tipo no puede terminar nunca. En consecuencia, la definición recursiva debe incluir un componente base (condición de salida) en el que f(n) se defina directamente (es decir, no recursivamente) para uno o más valores de n.

Debe existir una “forma de salir” de la secuencia de llamadas recursivas. Así en la función f(n) = n! para n entero

1 n ≤ 1

f(n){ n*f(n – 1) n > 1

la condición de salida o base es f(n) = 1 para n ≤ 1.

P:121

Recursividad 525

En el caso de la serie de Fibonacci

F0 = 0, F1 = 1, Fn = Fn-1 + Fn-2 para n > 1.

F0 = 0 y F1 = 1 constituyen el componente base o condiciones de salida y Fn = Fn-1 + Fn-2 es el componente recursivo.

C++ permite escribir funciones recursivas. Una función recursiva correcta debe incluir un componente base o

condición de salida.

PROBLEMA 14.1

Escribir una función recursiva en C++ que calcule el factorial de un número n y un programa que maneje dicha

función.

Recordemos que

n! = 1 si n = 0

n! = n*(n – 1)! si n ≥ 1

La función recursiva que calcula n!

int Factorial (int n)

{

// cálculo de n!

if (n <= 1)

return 1;

return n * Factorial(n – 1);

}

En el algoritmo anterior se ha considerado que el valor resultante es de tipo entero; sin embargo, observe la secuencia de valores de la función factorial.

n n!

0

1

2

3

4

5

6

7

8

9

10

1

1

2

6

24

120

720

5040

40320

362880

3628800

Como se puede ver, los valores crecen muy rápidamente, y para n = 8 ya sobrepasa el valor normal del mayor

entero manejado en computadoras de 16 bits (32767). Por consiguiente, será preciso cambiar el tipo de dato devuelto que ha de ser float, double, unsigned int, long, etc. En consecuencia, el programa fac.cpp que calcula

el factorial de un número puede ser:

//Programa fac.cpp

#include <iostream>

P:122

526 Fundamentos de programación

#using namespace std;

// en C++ las funciones han de ser declaradas

// o definidas antes de su uso

double Factorial (int n);

int main()

{

// declaración

int num;

// escribir ('Por favor introduzca un número: ')

cout << "Por favor introduzca un número: ";

// leer(num)

cin >> num;

// escribir (num, ' != ', Factorial(num); endl)

//endl significa salto de línea

cout << num <<" != " << Factorial(num) << endl;

// devolver exito, es decir ejecución válida

return 0;

}

// definición de la función Factorial

// el paso de parámetros de tipo simple por defecto es por valor

double Factorial (int n)

{

if (n <=1)

return (1);

else

return (n * Factorial(n – 1));

// en C++ los paréntesis en la sentencia return son opcionales

}

Una variante de este programa podría ser el cálculo del factorial correspondiente a los números naturales 0 a 10.

Para ello bastaría sustituir la función main anterior por una función tal como ésta e incluyendo una llamada al archivo #include <iomanip>

// Programa principal

int main()

{

int i;

for (i = 0; i<=10; i++)

cout << setw(2) << i <<" != " << Factorial(i) << endl;

// setw da formato a la salida y establece la anchura del

// campo a 2

return 0;

}

PROBLEMA 14.2

Escribir una función de Fibonacci de modo recursivo y un programa que manipule dicha función, de modo que calcule el valor del elemento de acuerdo a la posición ocupada en la serie.

P:123

Recursividad 527

Nota

El código fuente de este programa se ha escrito en lenguaje C++.

// Función de Fibonacci: fibo.cpp

#include <iostream>

#using namespace std;

long fibonacci (long n);

int main()

{

long resultado, num;

cout << "Introduzca un entero : ";

cin >> num;

resultado = fibonacci (num);

cout << "El valor de Fibonacci(" << num << ")= " << resultado << endl;

return 0;

}

// definición recursiva de la función de fibonacci

long fibonacci(long n)

{

if ((n == 0) || (n == 1))

return n;

// no es necesaria la especificación de else, pero se puede poner

return fibonacci(n - 1) + fibonacci (n - 2);

}

La salida resultante de la ejecución del programa anterior:

Introduzca un entero : 2

El valor de Fibonacci (2) = 1

Introduzca un entero : 20

El valor de Fibonacci (20) = 832040

14.2.1. Recursividad indirecta

La recursividad indirecta se produce cuando un subprograma llama a otro, que eventualmente terminará llamando de

nuevo al primero. El programa ALFABETO.CPP visualiza el alfabeto utilizando recursión mutua o indirecta.

Nota

El código fuente de este programa también se ha escrito en lenguaje C++.

// Listado ALFABETO.CPP

#include <iostream>

#include <stdio.h>

P:124

528 Fundamentos de programación

using namespace std;

// A y B equivalen a procedimientos

void A(int c);

void B(int c);

int main()

{

A('Z');

cout << endl;

return 0;

}

void A(int c)

{

if (c > 'A')

B(c);

putchar(c);

}

void B(int c)

{

A(--c);

}

El programa principal llama a la función recursiva A() con el argumento 'Z' (la última letra del alfabeto). La

función A examina su parámetro c. Si c está en orden alfabético después que 'A', la función llama a B(), que inmediatamente llama a A(), pasándole un parámetro predecesor de c. Esta acción hace que A() vuelva a examinar c,

y nuevamente una llamada a B(), hasta que c sea igual a 'A'. En este momento, la recursión termina ejecutando

putchar() veintiséis veces y visualizando el alfabeto, carácter a carácter.

14.2.2. Condición de terminación de la recursión

Cuando se implementa un subprograma recursivo será preciso considerar una condición de terminación, ya que en

caso contrario el subprograma continuaría indefinidamente llamándose a sí mismo y llegaría un momento en que la

memoria se podría agotar. En consecuencia, sería necesario establecer en cualquier subprograma recursivo la condición de parada que termine las llamadas recursivas y evitar indefinidamente las llamadas. Así, por ejemplo, en el caso

de la función factorial, definida anteriormente, la condición de salida puede ser cuando el número sea 1 o 0, ya

que en ambos casos el factorial es 1.

real función factorial(E entero: n)

inicio

si(n = 1) o (n = 0) entonces

devolver (1)

si_no

devolver (n * factorial (n - 1))

fin_si

fin_función

14.3. RECURSIÓN VERSUS ITERACIÓN

En las secciones anteriores se han estudiado varias funciones que se pueden implementar fácilmente o bien de modo

recursivo o bien de modo iterativo. En esta sección compararemos los dos enfoques y examinaremos las razones por

las que el programador puede elegir un enfoque u otro según la situación específica.

P:125

Recursividad 529

Tanto la iteración como la recursión se basan en una estructura de control: la iteración utiliza una estructura

repetitiva y la recursión utiliza una estructura de selección. La iteración y la recursión implican ambas repetición:

la iteración utiliza explícitamente una estructura repetitiva mientras que la recursión consigue la repetición mediante

llamadas repetidas. La iteración y recursión implican cada una un test de terminación (condición de salida). La iteración termina cuando la condición del bucle no se cumple mientras que la recursión termina cuando se reconoce un

caso base o la condición de salida se alcanza.

La recursión tiene muchas desventajas. Se invoca repetidamente al mecanismo de recursividad y en consecuencia

se necesita tiempo suplementario para realizar las mencionadas llamadas.

Esta característica puede resultar cara en tiempo de procesador y espacio de memoria. Cada llamada de una

función recursiva produce que otra copia de la función (realmente sólo las variables de función) sea creada; esto

puede consumir memoria considerable. Por el contrario, la iteración se produce dentro de una función, de mo do

que las operaciones suplementarias de las llamadas a la función y asignación de memoria adicional son omitidas.

En consecuencia, ¿cuáles son las razones para elegir la recursión? La razón fundamental es que existen numerosos problemas complejos que poseen naturaleza recursiva y, en consecuencia, son más fáciles de implementar con

algoritmos de este tipo. Sin embargo, en condiciones críticas de tiempo y de memoria, es decir, cuando el consumo

de tiempo y memoria sean decisivos o concluyentes para la resolución del problema, la solución a elegir debe ser,

normalmente, la iterativa.

Cualquier problema que se puede resolver recursivamente se puede resolver también iterativamente (no recursivamente). Un enfoque recursivo se elige normalmente con preferencia a un enfoque iterativo cuando el enfoque recursivo es más natural para la resolución del problema y produce un programa más fácil de comprender y depu rar. Otra razón para elegir una solución recursiva es que una solución iterativa puede no ser clara

ni evidente.

Consejo de programación

Se ha de evitar utilizar recursividad en situaciones de rendimiento crítico o exigencia de altas prestaciones en tiempo

y memoria, ya que las llamadas recursivas emplean tiempo y consumen memoria adicional.

Consejo de carácter general

Si una solución de un problema se puede expresar iterativa o recursivamente con igual facilidad, es preferi ble

la solución iterativa, ya que se ejecuta más rápidamente (no existen llamadas adicionales a funciones que

consumen tiempo de proceso) y utiliza menos memoria (la pila necesaria para almacenar las sucesivas llamadas necesarias en la recursión). Hay veces, sin embargo, que, pese a todo, es preferible la solución recursiva.

EJEMPLO 14.4

La función factorial de un número ya expuesta anteriormente ofrece un ejemplo claro de comparación entre funciones definidas de modo iterativo o modo recursivo y, a continuación, se muestra su implementación en C#.

El factorial n!, de un número n era

0! = 1

n! = n * (n - 1)! para n > 0

Solución recursiva

// código en C#

public class Prueba1

P:126

530 Fundamentos de programación

{

// factorial recursivo

// Precondición n está definido y n >= 0

// Postcondición ninguna

// Devuelve n!

public static long factorial(int n)

{

if (n < 0)

return – 1;

if (n == 0)

return 1;

else

return n * factorial(n – 1);

}

public static void main()

{

// escribir(factorial(4))

System.Console.WriteLine(factorial(4));

}

}

Solución iterativa

// código en C#

public class Prueba2

{

// factorial iterativo

// Precondición n está definido y n >= 0

// Postcondición ninguna

// Devuelve n!

public static long factorial(int n)

{

if (n < 0)

return – 1;

long fact = 1;

while (n > 0)

{

fact = fact * n;

n = n – 1;

}

return fact;

}

public static void main()

{

// escribir(factorial(4))

System.Console.WriteLine(factorial(4));

}

}

Directrices en la toma de decisión iteración/recursión

1. Considérese una solución recursiva sólo cuando una solución iterativa sencilla no sea posible.

2. Utilícese una solución recursiva sólo cuando la ejecución y eficiencia de la memoria de la solución esté dentro de límites aceptables considerando las limitaciones del sistema.

P:127

Recursividad 531

14.4. RECURSIÓN INFINITA

La iteración y la recursión pueden producirse infinitamente. Un bucle infinito ocurre si la prueba o test de continuación de bucle nunca se vuelve falsa; una recursión infinita ocurre si la etapa de recursión no reduce el problema en

cada ocasión de modo que converja sobre el caso base o condición de salida.

En realidad la recursión infinita significa que cada llamada recursiva produce otra llamada recursiva y ésta a su

vez otra llamada recursiva y así para siempre. En la práctica dicho código se ejecutará hasta que la computadora

agota la memoria disponible y se produzca una terminación anormal del programa.

El flujo de control de un algoritmo recursivo requiere tres condiciones para una terminación normal:

• Un test para detener (o continuar) la recursión (condición de salida o caso base).

• Una llamada recursiva (para continuar la recursión).

• Un caso final para terminar la recursión.

EJEMPLO 14.5

Se desea calcular la suma de los primeros N enteros positivos.

La función no recursiva que realiza la tarea solicitada es:

entero función CalculoSuma (E entero: N)

var

entero: suma, i

inicio

suma ← 0

desde i ← 1 hasta N hacer

suma ← suma + i

fin_desde

devolver (suma)

fin_función

La función CalculoSuma implementada recursivamente requiere la definición previa de la suma de los primeros

N enteros matemáticamente en forma recursiva, tal como se muestra a continuación:

1 si N = 1

suma(N) = { N + suma(N–1) en caso contrario

La definición anterior significa que si N es 1, entonces la función suma(N) toma el valor 1. En caso contrario,

significa que la función suma(N) toma el valor resultante de la suma de N y el resultado de suma(N–1). Por ejemplo,

la función suma(5) se evalúa tal como se muestra en la Figura 14.1 de la página siguiente.

El pseudocódigo fuente de la función recursiva suma es:

entero función suma(E entero: n)

inicio

// test para parar o continuar (condición de salida)

si (n = 1) entonces

devolver (1)

3. Si son posibles las dos soluciones, iterativa y recursiva, la solución recursiva siempre requerirá más tiempo

y espacio debido a las llamadas adicionales que se realizan.

4. En ciertos problemas, la recursión conduce naturalmente a soluciones que son mucho más fáciles de leer y

comprender que su correspondiente iterativa. En estos casos los beneficios obtenidos con la claridad de la

solución suelen compensar el coste extra (en tiempo y memoria) de la ejecución de un programa recursivo.

P:128

532 Fundamentos de programación

//caso final - se detiene la recursión

si_no

devolver(n + suma (n – 1))

//caso recursivo

//la recursión continúa con una llamada recursiva

fin_si

fin_función

y el código fuente en Turbo Pascal es:

program Sumas;

{solución interactiva}

function CalculoSuma (N: integer): integer;

var

suma, i: integer;

begin

suma := 0;

for i:= 1 to N do

suma := suma + i;

CalculoSuma := suma

end;

{solución recursiva}

function suma(n: integer): integer;

begin

{ test para parar o continuar (condición de salida)}

if n = 1 then

suma := 1

15

suma(5) = 5 + suma(4) 10

suma(4) = 4 + suma(3) 6

suma(3) = 3 + suma(2) 3

suma(2) = 2 + suma(1)

1

suma(1) = 1

Figura 14.1. Secuencia de llamadas recursivas que evalúan la función Suma(N) (en el ejemplo Suma(4)).

P:129

Recursividad 533

{ caso final – se detiene la recursión}

la recursión continúa con una llamada recursiva }

else

suma := n + suma (n – 1);

end;

begin

writeln('Suma recursiva ', suma(4))

writeln('Suma iterativa ', CalculoSuma(4))

end

Cuando se realizan llamadas recursivas se han de pasar argumentos diferentes de los parámetros de entrada; así,

en el ejemplo de la función suma, el argumento que se pasa en la función recursiva es n – 1 y el parámetro es n. La

Figura 14.2 muestra el flujo de control de la función suma de modo recursivo.

n = 5

ent suma(int n)

inicio

si(n = 1)

devolver 1;

sino

devolver n + suma(n – 1);

fin

ent suma(int n)

inicio

si(n = 1)

devolver 1;

sino

devolver n + suma(n – 1);

fin

15

n = 4

10

6 n = 3

...

Figura 14.2. Flujo de control de la función suma recursiva.

PROBLEMA 14.2

Deducir cuál es la condición de salida de la función mcd() que calcula el mayor divisor común de dos números enteros b1 y b2 (el mcd, máximo común divisor, es el entero mayor que divide a ambos números) y un programa que

la manipule.

El mcd de los enteros b1 y b2 se define como el entero mayor que divide a ambos números. El mcd no está definido si b1 y b2 son cero. Los valores negativos de b1 y b2 se sustituyen por su valores absolutos. Supongamos dos

números 6 y 124; el procedimiento clásico de obtención del mcd es la realización de divisiones sucesivas, se comienza dividiendo ambos números (124 entre 6) si el resto no es 0, se divide el número menor por el resto y así sucesivamente hasta que el resto sea 0.

P:130

534 Fundamentos de programación

124 6

04 20

64 42

21 02

(mcd = 2)

20 1 2

124 6 4 2

420

mcd = 2

En el caso de 124 y 6, el mcd es 2. Suponga ahora que los números son b1=18 y b2=45

18 45

18 0

45 18

09 2

18 9

02

(mcd = 9)

El mcd de 18 y 45 es 9. En consecuencia, la condición de salida es que el resto sea cero. Por tanto:

1. Si b2 es cero, la solución es b1.

2. Si b2 no es cero, la solución es mcd(b2, b1 mod b2).

El código fuente de la función es:

entero función mcd(E entero: b1, b2)

inicio

si (b2 <> 0) entonces // condición de salida

devolver ( mcd (b2, b1 mod b2))

si_no

devolver (b1)

fin_si

fin_función

Un programa en C++ que gestiona la función mcd es mcd.cpp.

#include <iostream>

using namespace std;

// Programa mcd.cpp, escrito en lenguaje C++

int mcd(int n, int m);

void main()

{

// datos locales

int m, n;

cout << "Introduzca dos enteros positivos :";

cin >> m >> n;

cout << endl;

cout << "El máximo común divisor es : " << mcd(m, n) << endl;

}

// Función recursiva mcd

int mcd(int n,int m)

// devuelve el máximo común divisor de m y n

P:131

Recursividad 535

{

if (m != 0) // condición de salida

return mcd(m, n % m);

else

return n;

} // final de mcd

Al ejecutarse el programa se produce la siguiente salida:

Introduzca dos enteros positivos : 6 40

El máximo común divisor es : 2

El código de la función recursiva en Turbo Pascal es

function mcd (n, m: integer): integer;

begin

if m <> 0 then

mcd := mcd(m, n mod m)

else

mcd := n

end;

14.5. RESOLUCIÓN DE PROBLEMAS COMPLEJOS CON RECURSIVIDAD

Muchos problemas de computadora tienen una formulación simple y elegante que se traduce directamente a código

recursivo. En esta sección se describen una serie de ejemplos que incluyen problemas clásicos resueltos mediante

recursividad. Entre ellos se destacan problemas matemáticos, las Torres de Hanoi, método de búsqueda binaria, ordenación rápida, árboles de expresión, etc. Explicamos con detalle algunos de ellos.

14.5.1. Torres de Hanoi

Este juego (un algoritmo clásico) tiene sus orígenes en la cultura oriental y en una leyenda sobre el Templo de Brahma. El problema en cuestión supone la existencia de 3 varillas o postes en los que se alojaban discos, cada disco es

ligeramente inferior en diámetro al que está justo debajo de él, y pretende determinar los movimientos necesarios

para trasladar los discos de una varilla a otra cumpliendo las siguientes reglas:

• En cada movimiento sólo puede intervenir un disco.

• Nunca puede quedar un disco sobre otro de menor tamaño.

La Figura 14.3 ilustra el problema. Los cuatro discos situados en la varilla I se desean trasladar a la varilla F

conservando la condición de que cada disco sea ligeramente inferior en diámetro al que tiene situado debajo de él.

varilla I varilla C varilla F

Figura 14.3.

P:132

536 Fundamentos de programación

Este problema es claramente recursivo, pues mover cuatro discos de la varilla I a la F consiste en trasladar los

tres discos superiores de la varilla origen a otra considerada como auxiliar (C). Figuras 14.3 y 14.4,

varilla I varilla C varilla F

Figura 14.4.

trasladar el disco más grande de la varilla origen al destino (de I a F). Figuras 14.4 (antes) y 14.5 (después).

varilla I varilla C varilla F

Figura 14.5.

y pasar los tres de la varilla auxiliar al destino. Figura 14.5 (antes) y 14.6 (después).

varilla I varilla C varilla F

Figura 14.6.

Nuevamente se observa que mover los tres discos superiores de un origen a un destino requiere mover dos de

origen a auxiliar, uno de origen a destino y dos de auxiliar a destino. Por último, trasladar dos discos de origen a

destino implica trasladar uno de origen a auxiliar, otro de origen a destino y completar la operación pasando el de la

varilla auxiliar a destino.

Los movimientos que se realizarían detallados gráficamente para el caso de N = 3, son

I C F

situación

inicial

P:133

Recursividad 537

I C F

2.o

I C F

3.o

4.o

I C F

I C F

1.o

5.o

I C F

P:134

538 Fundamentos de programación

I C F

6.º

ICF

7.º

Diseño del algoritmo

El algoritmo se escribe generalizando para n discos y tres varillas. La función de Hanoi declara las varillas o postes

como objetos cadena. En la lista de parámetros, el orden de las variables o varillas es:

varinicial varcentral varfinal

lo que implica que se están moviendo discos desde la varilla inicial a la final utilizando la varilla central como auxiliar para almacenar los discos. Si n = 1 se tiene la condición de parada, ya que se puede manejar moviendo el único

disco desde la varilla inicial a la varilla final. El algoritmo sería el siguiente:

1. Si n es 1

1.1 Mover el disco 1 de varinicial a varfinal

2. Si_no

1.2 Mover n – 1 discos desde varinicial hasta la varilla auxiliar utilizando

varfinal

1.3 Mover el disco n desde varinicial a varfinal

1.4 Mover n – 1 discos desde la varilla auxiliar o central a varfinal utilizando

la varilla inicial.

Es decir, si n es 1, se alcanza la condición de salida o terminación del algoritmo. Si n es mayor que 1, las etapas

recursivas 1.2, 1.3 y 1.4 son tres subproblemas más pequeños, que aproximan a la condición de salida.

Las Figuras 14.7, 14.8 y 14.9 muestran el algoritmo anterior:

Etapa 1: Mover n – 1 discos desde varilla inicial (I).

varilla inicial varilla central varilla final

Figura 14.7.

P:135

Recursividad 539

Etapa 2: Mover un disco desde I a F.

varilla inicial varilla central varilla final

Figura 14.8.

Etapa 3: Mover n – 1 discos desde varilla central (C).

varilla inicial varilla central varilla final

Figura 14.9.

La primera etapa en el algoritmo mueve n – 1 discos desde la varilla inicial a la varilla central utilizando la varilla final. Por consiguiente, el orden de parámetros en la llamada a la función recursiva es varinicial, varfinal y

varcentral.

// utilizar varfinal como almacenamiento auxiliar

Hanoi(n – 1, varinicial, varfinal, varcentral);

La segunda etapa mueve simplemente el disco mayor desde la varilla inicial a la varilla final:

escribir "mover", varinicial, "a", varfinal, fin de línea;

La tercera etapa del algoritmo mueve n – 1 discos desde la varilla central a la varilla final utilizando varinicial

para almacenamiento temporal. Por consiguiente, el orden de parámetros en la llamada a la función recursiva es:

varcentral, varinicial y varfinal.

// utilizar varinicial como almacenamiento auxiliar

Hanoi(n – 1, varcentral, varinicial, varfinal);

Implementación de las Torres de Hanoi en C++

La implementación del algoritmo se apoya en los nombres de las tres varillas o alambres "inicial", "central"

y "final" que se pasan como parámetros a la función. El programa comienza solicitando al usuario que introduzca

el número de discos N. Se llama a la función recursiva Hanoi para obtener un listado de los movimientos que transferirán los N discos desde la varilla "inicial" a la varilla "final". El algoritmo requiere 2N – 1 movimientos. Para

el caso de 10 discos, el juego requerirá 1.023 movimientos. En el caso de prueba para N = 3, el número de movimientos es 23

– 1 = 7.

P:136

540 Fundamentos de programación

// archivo Torres.cpp

// función recursiva Torres de Hanoi

void Hanoi(char varinicial, char varfinal, char varcentral, int n)

{

if ( n == 1)

cout << " Mover disco 1 de varilla " << varinicial << " a varilla "

<< varfinal << endl;

else

{

Hanoi(varinicial, varcentral, varfinal, (n - 1);

cout << " Mover disco " << n << " desde varilla " << varinicial <<

" a varilla " << varfinal << endl;

Hanoi(varcentral, varfinal, varinicial, n - 1);

}

}

Una ejecución de la función para el caso de mover tres discos desde las varillas A a C tomando la varilla B como

varilla central o auxiliar, se puede conseguir con la siguiente sentencia:

Hanoi ('A', 'C', 'B', 3);

que resuelve el problema de tres discos desde A a C. La salida generada sería:

Mover disco 1 de varilla A a varilla C

Mover disco 2 de varilla A a varilla B

Mover disco 1 de varilla C a varilla B

Mover disco 3 de varilla A a varilla C

Mover disco 1 de varilla B a varilla A

Mover disco 2 de varilla B a varilla C

Mover disco 1 de varilla A a varilla C

Consideraciones de eficiencia en las Torres de Hanoi

Es de destacar que la función Hanoi resolverá el problema de las Torres de Hanoi para cualquier número de discos.

El problema de tres discos se resuelve en un total de 7 (23

– 1) llamadas a la función Hanoi mediante 7 movimientos de disco. El problema de cinco discos se resuelve con 31 (25

– 1) llamadas y 31 movimientos. En general, como

ya se ha expresado anteriormente, el número de movimientos requeridos para resolver el problema de n discos es

2n

– 1. Cada llamada a la función requiere la asignación e inicialización de un área local de datos en la memoria, por

lo que el tiempo de computadora se incrementa exponencialmente con el tamaño del problema. Por estas razones, la

ejecución del programa con un valor de n mayor que 10 requiere gran cantidad de prudencia para evitar desbordamientos de memoria y ralentización de tiempo.

14.5.2. Búsqueda binaria recursiva

Recordemos que la búsqueda binaria era aquel método de búsqueda de una clave especificada dentro de una lista o

array ordenado de n elementos que realizaba una exploración de la lista hasta que se encontraba o no la coincidencia

con la clave especificada. El algoritmo de búsqueda binaria se puede describir recursivamente.

Supóngase que se tiene una lista ordenada A con un límite inferior y un límite superior. Dada una clave (valor

buscado) se comienza la búsqueda en la posición central de la lista (índice central).

Inferior Central Superior

Central = (inferior + superior)div 2 Comparar A[central] y clave

P:137

Recursividad 541

Si se produce coincidencia (se encuentra la clave), se tiene la condición de terminación que permite detener la

búsqueda y devolver el índice central. Si no se produce la coincidencia (no se encuentra la clave), dado que la lista

está ordenada, se centra la búsqueda en la “sublista inferior” (a la izquierda de la posición central) o en la “sublista

derecha” (a la derecha de la posición central).

1. Si clave < A[central], el valor buscado sólo puede estar en la mitad izquierda de la lista con elementos

en el rango inferior a central – 1.

Clave •

Inferior Central Superior

Búsqueda en sublista izquierda

[inferior..central -1]

2. Si clave > A[central], el valor buscado sólo puede entrar en la mitad derecha de la lista con elementos

en el rango de índices, Central + 1 a Superior.

Clave •

Inferior Central Superior

Búsqueda en sublista derecha

[central + 1..superior]

3. El proceso recursivo continúa la búsqueda en sublistas más y más pequeñas. La búsqueda termina o con éxito (aparece la clave buscada) o sin éxito (no aparece la clave buscada), situación que ocurrirá cuando el límite superior de la lista sea más pequeño que el límite inferior. La condición Inferior > Superior será

la condición de salida o terminación y el algoritmo devuelve el índice – 1.

En notación matemática y algorítmica se podría representar la búsqueda binaria de la siguiente forma:

BusquedaBR(inferior, superior, clave) // BR, binaria recursiva

=

{ devolver no encontrada

si inferior > superior

devolver central

si elemento[central] = clave

devolver BusquedaBR(central + 1, superior, clave)

si elemento[central] < clave

devolver BusquedaBR(inferior, central - 1, clave

si elemento[central] > clave

en donde central es el punto central entre inferior y superior. Su codificación en Java podría ser:

public class Bbin

{

private int busquedaBinaria(int[] a, int iz, int de, int c)

{

int central;

if (de < iz)

return(– 1);

else

{

central = (iz + de)/2;

if (c < a[central])

return(busquedaBinaria(a, iz, central – 1, c));)

P:138

542 Fundamentos de programación

else

if (a[central] < c)

return(busquedaBinaria(a, central + 1, de, c));

else

return(central);

}

}

public int búsquedaB(int[] a, int c)

{

// los arrays en Java comienzan con el subíndice 0

// a.length se encuentra predefinido y devuelve

// la longitud del array

return(busquedaBinaria(a,0,a.length – 1,c));

}

}

14.5.3. Ordenación rápida (QuickSort)

El algoritmo conocido como quicksort (ordenación rápida) recibe su nombre de su autor, Tony Hoare. La idea del algoritmo es simple, se basa en la división en particiones de la lista a ordenar. El método es, posiblemente, el más pequeño de código, más rápido, más elegante y más interesante y eficiente de los algoritmos conocidos de ordenación.

El método se basa en dividir los n elementos de la lista a ordenar en tres partes o particiones: una partición izquierda, una partición central que sólo contiene un elemento denominado pivote o elemento de partición y una partición derecha. La partición o división se hace de tal forma que todos los elementos de la primera sublista (partición

izquierda) son menores que todos los elementos de la segunda sublista (partición derecha). Las dos sublistas se ordenan entonces independientemente.

La lista se divide en particiones (sublistas) eligiendo uno de los elementos de la lista y se utiliza como pivote o

elemento de partición. Si se elige una lista cualquiera con los elementos en orden aleatorio, se puede elegir cualquier

elemento de la lista como pivote; por ejemplo, el primer elemento de la lista. Si la lista tiene algún orden parcial, que

se conoce, se puede tomar otra decisión para el pivote. Idealmente, el pivote se debe elegir de modo que se divida la

lista exactamente por la mitad, de acuerdo al tamaño relativo de las claves. Por ejemplo, si se tiene una lista de enteros de 1 a 10, 5 o 6 serían pivotes ideales, mientras que 1 o 10 serían elecciones “pobres” de pivotes.

Una vez que el pivote ha sido elegido, se utiliza para ordenar el resto de la lista en dos sublistas: una tiene todas

las claves menores que el pivote y la otra en la que todos los elementos (claves) son mayores que o iguales que el

pivote (o al revés). Estas dos listas parciales se ordenan recursivamente utilizando el mismo algoritmo; es decir, se

llama sucesivamente al propio algoritmo quicksort. La lista final ordenada se consigue concatenando la primera sublista, el pivote y la segunda lista, en ese orden, en una única lista. La primera etapa de quicksort es la división o

“particionado” recursivo de la lista hasta que todas las sublistas constan de sólo un elemento.

EJEMPLO 14.6

1. lista inicial

pivote elegido

2 96 18 38 12 45 10 55 81 43 39

39

2 10 18 38 12 39 96 55 81 43 45

<= pivote >= pivote

pivote

2. lista inicial 13 81 92 43 65 31 57 26 75 0

pivote

0 13 92 43 65 31 57 26 75 81

<= pivote >= pivote

pivote

P:139

Recursividad 543

EJEMPLO 14.7 (Pivote: primer elemento de la lista)

1. Lista original 5218379

pivote elegido 5

sublista izquierda1, Izqda1 (elementos menores que 5) 213

sublista derecha1, Dcha1 (elementos mayores o iguales a 5) 879

2. Sublista Izda1 213

sublista Izda2 1

pivote2 sublista Dcha2 3

Sublista Izda1 Izda pivote2 Dcha

123

3. Sublista Dcha1 879

sublista Izda2 7

pivote3 sublista Dcha2 9

Sublista Dcha1 Izda pivote3 Dcha

789

4. Lista ordenada final

Sublista izquierda Pivote Sublista derecha

1 2 3 5 7 8 9

El algoritmo quicksort requiere una estrategia de partición y la selección idónea del pivote. Las etapas fundamentales del algoritmo dependen del pivote elegido, aunque la estrategia de partición suele ser similar. La primera etapa

en el algoritmo de partición es obtener el elemento pivote; una vez que se ha seleccionado se ha de buscar el sistema

para situar en la sublista izquierda todos los elementos menores o iguales que el pivote y en la sublista derecha todos

los elementos mayores que el pivote y dejar el pivote como separador de ambas sublistas.

EJEMPLO 14.8

Lista: 8 1 4 9 6 3 5 2 7 0

Etapa 1:

En esta etapa se efectúa la selección del pivote. Lo primero que se hace es calcular la posición central y si el

primer elemento es mayor que el central se intercambian,

Lista: 6 1 4 9 8 3 5 2 7 0

si el primer elemento es mayor que el último, se intercambian

Lista: 0 1 4 9 8 3 5 2 7 6

P:140

544 Fundamentos de programación

si el central es mayor que el último, se intercambian.

Lista: 0 1 4 9 6 3 5 2 7 8

Se toma ahora el central como pivote y se intercambia con el elemento extremo.

Pivote 6

Lista: 0 1 4 9 8 3 5 2 7 6

Etapa 2:

La etapa 2 requiere mover todos los elementos menores al pivote, entre el primero y el penúltimo, a la parte izquierda del array y los elementos mayores a la parte derecha.

0 1 4 9 8 3 5 2 7 6

Para ello se recorre la lista de izquierda a derecha utilizando un contador i que se inicializa en la posición más

baja (Inferior) buscando un elemento mayor al pivote. También se recorre la lista de derecha a izquierda buscando un elemento menor. Para hacer esto se utilizará un contador j inicializado en la posición más alta, Superior-1.

El contador i se detiene en el elemento 9 (mayor que el pivote) y el contador j se detiene en el elemento 2 (menor que el pivote).

0 1 4 9 8 3 5 2 7 6

i j

Ahora se intercambian 9 y 2 para que estos dos elementos se sitúen correctamente en cada sublista.

0 1 4 2 8 3 5 9 7 6

A medida que el algoritmo continúa, i se detiene en el elemento mayor, 8, y j se detiene en el menor, 5.

0 1 4 2 8 3 5 9 7 6

i j

Se intercambian los elementos mientras que i y j no se cruzan, por tanto se intercambian 8 y 5.

0 1 4 2 8 3 5 9 7 6

Continúa la exploración.

0 1 4 2 5 3 8 9 7 6

j i

En esta posición los contadores i y j se encuentran sobre el mismo elemento del array y en este caso se detiene

la búsqueda y no se realiza ningún intercambio, ya que el elemento al que accede el contador j está ya correctamente situado. Las dos sublistas ya han sido creadas (la lista original se ha dividido en dos particiones).

0 1 4 2 5 3 8 9 7 6

i–1 i i+1

Ahora ya lo único que se necesita es intercambiar el elemento que está en la posición i con el elemento pi vote.

P:141

Recursividad 545

Etapa 3:

Intercambiar el elemento de la posición i con el pivote, de modo que se tendrá la secuencia prevista inicialmente:

0 1 4 2 5 3 6 9 7 8

sublista izquierda pivote sublista derecha

Resumiendo el proceso general sería:

8149635270

8

7 9

5

012

3

5 4

4

6

014253 978

0 2

1

14.5.3.1. Algoritmo quicksort

El primer problema a resolver en el diseño del algoritmo de quicksort es seleccionar el pivote. Aunque la posición

del pivote, en principio puede ser cualquiera, una de las decisiones más ponderadas es aquella que considera el pivote como el elemento central o próximo al central de la lista. La Figura 14.10 muestra las operaciones del algoritmo

para ordenar la lista de elementos enteros L.

// algoritmo quicksort

// ordenar a[0:n-1]

Seleccionar un elemento de a[0:n-1] como elemento central

(este elemento es el pivote)

Dividir los elementos restantes en particiones izquierda y derecha,

de modo que ningún elemento de la izquierda tenga una clave (valor) mayor que

el pivote y que ningún elemento a la derecha tenga una clave más pequeña que la

del pivote.

Ordenar la partición izquierda utilizando quicksort recursivamente.

Ordenar la partición derecha utilizando quicksort recursivamente.

14.5.4. Ordenación MERGESORT

La idea básica de este método de ordenación es la mezcla (merge) de listas ya ordenadas. El algoritmo puede considerarse que aplica la técnica “divide y vence”, el proceso es simple: si se ordena la primera mitad de la lista, se ordena la segunda mitad de la lista y una vez ordenadas se mezclan, la mezcla da lugar a una lista de elementos ordenada. A su vez, la ordenación de la sublista mitad sigue los mismos pasos, ordenar la primera mitad, ordenar la

segunda mitad y mezclar. La sucesiva división de la lista actual en dos hace que el problema (número de elementos)

cada vez sea mas pequeño; así hasta que la lista actual tenga un elemento y, por tanto, se considera ordenada, es el

caso base y a partir de dos sublistas de un número mínimo de elementos se mezclan, dando cada vez lugar a listas

ordenadas de cada vez más elementos hasta alcanzar la lista total.

P:142

546 Fundamentos de programación

Es decir, que el método consiste, pues, en dividir el vector por su posición central en dos partes y tratar análogamente cada una de ellas hasta que consten de un único elemento. Hay que tener en cuenta que un vector con un

único elemento siempre se encuentra ordenado. A la salida de los procesos recursivos las partes ordenadas (subvectores) se mezclan de forma que resultan otras, de mayor longitud, también ordenadas.

EJEMPLO 14.9

Seguir la estrategia del algoritmo «mergesort» para ordenar la lista:

9 1 3 5 10 4 6

Se representa el proceso con las siguientes figuras en las que aparecen las divisiones.

9 1 3 5 10 4 6

9135 10 4 6

9 1 3 5 10 4 6

9 1 3 5 10 4

La mezcla comienza con las sublistas de un solo elemento, que dan lugar a otra sublista del doble de elementos

ordenados. El proceso continúa hasta que se construye un única lista ordenada. A continuación se muestra la creación

de las sublistas ordenadas:

1 3 4 5 6 9 10

1359 4 6 10

1 9 3 5 4 10

9 13 5 10 4 6

14.5.4.1. Algoritmo mergesort en JAVA

Este algoritmo de ordenación se diseña fácilmente con ayuda de las llamadas recursivas para dividir las listas en dos

mitades; posteriormente se invoca al método de mezcla de dos listas ordenadas. La delimitación de las dos listas se

puede hacer con tres índices: primero, central y último, que apuntan a los elementos del array significados por

los identificadores. Así, si se tiene una lista de 10 elementos los valores de los índices:

primero = 0; ultimo = 9; central = (primero+ultimo)/2 = 4

La primera sublista comprende los elementos a0 .. a4 y la segunda los elementos siguientes a4+1 .. a9. Los

pasos del algoritmo mergesort para el array (arreglo) a:

P:143

Recursividad 547

procedimiento mergesort(E/S arr: a, E entero: primero, ultimo)

inicio

Si primero < ultimo entonces

central ← (primero+ultimo) div 2

mergesort(a, primero, central)

// ordena primera mitad de la lista

mergesort(a, central+1, ultimo)

// ordena segunda mitad de la lista

mezcla(a, primero, central, ultimo)

{fusiona las dos sublistas ordenadas, delimitadas por

los extremos}

fin_si

fin_procedimiento

La codificación en Java consta del método mergesort() y del método auxiliar mezcla().

public class PruebaMS

{

public void mergesort(double[] a, int primero, int ultimo)

{

int central;

if (primero < ultimo)

{

central = (primero+ultimo)/2;

// división entera puesto que los operandos son enteros

mergesort(a, primero, central);

mergesort(a, central+1, ultimo);

mezcla(a, primero, central, ultimo);

}

}

private void mezcla(double[] a, int izda, int medio, int drcha)

{

double [] tmp = new double[a.length];

int x, y, z;

x = z = izda;

y = medio+1;

// bucle para la mezcla, utiliza tmp[] como array auxiliar

while (x<=medio && y<=drcha)

{

if (a[x] <= a[y])

tmp[z++] = a[x++];

else

tmp[z++] = a[y++];

}

// bucle para mover elementos que quedan de sublistas

while (x <= medio)

tmp[z++] = a[x++];

while (y <= drcha)

tmp[z++] = a[y++];

// Copia de elementos de tmp[] al array a[]

System.arraycopy(tmp, izda, a, izda, drcha-izda+1);

}

P:144

548 Fundamentos de programación

CONCEPTOS CLAVE

• Clases de recursividad: directa e

indirecta

• Concepto de recursividad.

• Complejidad de los métodos de

ordenación.

• Eficiencia en cuanto al tiempo de

ejecución

• Iteración versus recursión.

• Notación O.

• Requisitos de un algoritmo

recursivo.

RESUMEN

Un subprograma se dice que es recursivo si tiene una o más

sentencias que son llamadas a sí mismo. La recursividad

puede ser directa e indirecta, la recursividad indirecta ocurre cuando el subprograma —método, procedimiento o función— f() llama a p() y éste a su vez llama a f(). La

recursividad es una alternativa a la iteración en la resolución

de algunos problemas matemáticos. Los aspectos más importantes a tener en cuenta en el diseño y construcción de

métodos recursivos son los siguientes:

• Un algoritmo recursivo correspondiente con un método normalmente contiene dos tipos de casos: uno o

más casos que incluyen al menos una llamada recursiva y uno o más casos de terminación o parada del

problema en los que éste se soluciona sin ninguna

llamada recursiva sino con una sentencia simple. De

otro modo, un método recursivo debe tener dos partes:

una parte de terminación en la que se deja de hacer

llamadas, es el caso base, y una llamada recursiva con

sus propios parámetros.

• Muchos problemas tienen naturaleza recursiva y la

solución más fácil es mediante un método recursivo.

De igual modo, aquellos problemas que no entrañen

una solución recursiva se deberán seguir resolviendo

mediante algoritmos iterativos.

• Todo algoritmo recursivo puede ser transformado en

otro de tipo iterativo, pero para ello a veces se nece-

sita utilizar pilas donde almacenar los cálculos parciales.

• Los métodos con llamadas recursivas utilizan memoria extra en las llamadas; existe un límite en las

llamadas, que depende de la memoria de la computadora. En caso de superar este límite ocurre un error de

overflow.

• Cuando se codifica un método recursivo se debe comprobar siempre que tiene una condición de terminación; es decir, que no se producirá una recursión infinita. Durante el aprendizaje de la recursividad es usual

que se produzca ese error.

• Para asegurarse de que el diseño de un método recursivo es correcto se deben cumplir las siguientes tres

condiciones:

1. No existe recursión infinita. Una llamada recursiva

puede conducir a otra llamada recursiva y ésta conducir a otra, y así sucesivamente; pero cada llamada debe de aproximarse más a la condición de

terminación.

2. Para la condición de terminación, el método devuelve el valor correcto para ese caso.

3. En los casos que implican llamadas recursivas: si

cada uno de los métodos devuelve un valor correcto, entonces el valor final devuelto por el método

es el valor correcto.

public static void main(String[] args)

{

PruebaMS up=new PruebaMS();

double[] a = {9,1,3,5,10,4,6};

up.mergesort(a, 0, a.length-1);

for (int i = 0; i < a.length; i++)

System.out.println(a[i]);

}

}

P:145

Recursividad 549

EJERCICIOS

14.1. La suma de una serie de números consecutivos de 1

se puede definir recursivamente como:

suma(1) = 1

suma(n) = n + suma(n-1)

Escribir la función recursiva que acepte n como un

argumento y calcule la suma de los números de 1 a n.

14.2. El valor de xn se puede definir recursivamente

como:

x0

= 1

xn

= x * xn – 1

Escribir una fu nción recursiva que calcule y devuelva el valor de xn

.

14.3. Reescribir la función escrita en el Ejercicio 14.2 de

modo que se utilice un algoritmo repetitivo para calcular el valor de xn

.

14.4. Convierta la siguiente función iterativa en una recursiva. La función calcula un valor aproximado de e, la

base de los logaritmos naturales, sumando las series

1 + 1/1! + 1/2! + ... + 1/n!

hasta que los términos adicionales no afecten a la

aproximación

real función loge()

var

// Datos locales

real: enl, delta, fact

entero: n

inicio

enl ← 1.0

fact ← 1.0

delta ← 1.0

hacer

enl ← delta

n ← n + 1

fact ← fact * n

delta ← 1.0 / fact

mientras (enl <> enl + delta)

devolver (enl)

fin_función

14.5. Explique por qué la siguiente función puede producir

un valor incorrecto cuando se ejecute:

real función factorial (E real: n)

inicio

si (n = 0 o n = 1) entonces

devolver(1)

si_no

devolver (n * factorial (n-1))

fin_si

fin_función

14.6. Proporcionar funciones recursivas que representen

los siguientes conceptos:

a) El producto de dos números naturales.

b) El conjunto de permutaciones de una lista de

números.

14.7. El elemento mayor de un array entero de n-elementos

se puede calcular recursivamente. Definir la función:

entero funcion max(E entero: x, y)

que devuelve el mayor de dos enteros x e y. Definir

la función

entero función maxarray(E arr: a,

E entero: n)

que utiliza recursión para devolver el elemento mayor de a

Condición de parada: n == 1

Incremento recursivo: maxarray =

max(max(a[0]...

a[n–2]),a[n–1])

PROBLEMAS

14.1. La expresión matemática C(m, n) en el mundo de la

teoría combinatoria de los números representa el

número de combinaciones de m elementos tomados

de n en n elementos

C(m, n) = m!

n!(m – n)!

Escribir y probar una función que calcule C(m, n)

donde n! es el factorial de n.

14.2. Un palíndromo es una palabra que se escribe exactamente igual leído en un sentido o en otro. Palabras

tales como level, deed, ala, etc., son ejemplos de

palíndromos. Escribir una función recursiva que devuelva un valor de 1 (verdadero), si una palabra

pasada como argumento es un palíndromo y devuelva 0 (falso) en caso contrario.

P:146

550 Fundamentos de programación

14.3. La suma de los primeros n números enteros responde a la fórmula:

1 + 2 + 3 + ... + n = n(n + 1)/2

Inicializar el array A que contiene los primeros 50

en teros. La media de estos elementos del array es

entonces 51/2 = 25.5. Comprobar la solución aplicando la función recursiva media (media (float

a[], int n)).

14.4. Leer un número entero positivo n < 10. Calcular el

desarrollo del polinomio (x + 1)n

. Imprimir cada potencia x2

en la forma x**i.

Sugerencia:

(x + 1)n

= Cn,nxn

+ Cn, n – 1xn – 1 + Cn, n – 2xn – 2 + ...+

+ Cn, 2x2

+ Cn – 1x1

+ Cn10x0

donde Cn,n y Cn,0 son 1 para cualquier valor de n.

La relación de recurrencia de los coeficientes binomiales es:

C(n, 0) = 1

C(n, n) = 1

C(n, k) = C(n – 1, k – 1) + C(n – 1, k)

Estos coeficientes constituyen el famoso Triángulo

de Pascal y será preciso definir la función que genera el trián gulo

1

1 1

1 2 1

1 3 3 1

1 4 6 4 1

...

14.5. Escribir un programa en el que el usuario introduzca 10 enteros positivos y calcule e imprima su factorial.

P:147

551 Fundamentos de programación

PARTE III

Programación

orientada a objetos

y UML 2.1

CONTENIDO

Capítulo 15. Tipos abstractos de datos, objetos y modelado con UML 2.1

Capítulo 16. Diseño de clases y objetos: Representaciones gráficas en UML

Capítulo 17. Relaciones entre clases: delegaciones, asociaciones, agregaciones,

herencia

P:149

CAPÍTULO 15

Tipos abstractos de datos, objetos

y modelado con UML 2.1

15.1. Programación estructurada (procedimental)

15.2. Programación orientada a objetos

15.3. Modelado e identificación de objetos

15.4. Propiedades fundamentales de orientación

a objetos

15.5. Modelado de aplicaciones: UML

15.6. Diseño de software con UML

15.7. Historia de UML

15.8. Terminología de orientación a objetos

CONCEPTOS CLAVE

RESUMEN

EJERCICIOS

La Programación Orientada a Objetos (POO) es un

enfoque conceptual específico para diseñar programas, utilizando un lenguaje de programación orientado a objetos, en nuestro caso C++ o Java. Las propiedades más importantes de la POO son:

• Abstracción.

• Encapsulamiento y ocultación de datos.

• Polimorfismo.

• Herencia.

• Reusabilidad o reutilización de código.

Este paradigma de programación viene a superar

las limitaciones que soporta la programación tradicional o "procedimental" y por esta razón se comenzará el capítulo con una breve revisión de los

conceptos fundamentales de este paradigma de programación.

Los elementos fundamentales de la POO son las

clases y objetos. En esencia, la POO se concentra en el

objeto tal como lo percibe el usuario, pensando en los

datos que se necesitan para describir el objeto y las

operaciones que describirán la iteración del usuario

con los datos. Después se desarrolla una descripción

de la interfaz externa y se decide cómo implementar

la interfaz y el almacenamiento de datos. Por último,

se ponen juntos en un programa que utilice su nuevo

dueño.

En este texto nos limitaremos al campo de la programación, pero es también posible hablar de sistemas de administración de bases de datos orientadas

a objetos, sistemas operativos orientados a objetos,

interfaces de usuarios orientadas a objetos, etc.

En el capítulo se hace una introducción a UML,

como Lenguaje Unificado de Modelado. UML se ha

convertido de facto en el estándar para modelado de

aplicaciones software y es un lenguaje con sintaxis y

semántica propias, se compone de pseudocódigo, código real, programas,… Se describe también en el

capítulo una breve historia de UML desde la ya mítica

versión 0.8 hasta la actual versión 2.1 con su última

actualización, versión 2.1.1.

INTRODUCCIÓN

P:150

554 Fundamentos de programación

15.1. PROGRAMACIÓN ESTRUCTURADA (PROCEDIMENTAL)

La programación estructurada que se ha estudiado en profundidad hasta este momento viene representada por lenguajes procedimentales clásicos como Pascal y C, aunque los más antiguos como FORTRAN, COBOL o BASIC también

pertenecen a esta categoría. En estos lenguajes, cada sentencia (instrucción) del lenguaje indica a la com putadora que

debe realizar alguna acción o tarea. “Obtener una entrada”, “sumar dos números”, “dividir por cinco”, “visualizar la

salida”, etc. En un lenguaje procedimental, un programa es una lista (conjunto) de instrucciones o sentencias.

En el caso de pequeños programas, no se necesita ningún otro principio de organización. El programador crea

una lista de instrucciones y una computadora las ejecuta. Cuando los programas se vuelven más complejos, la lista

de instrucciones se vuelve grande e inmanejable ya que fácilmente se alcanzan decenas o centenas de instrucciones.

En este caso el programa se rompe en unidades más pequeñas que se hagan más comprensibles a las personas que

lo utilizan. Estas unidades en C (precursor de C++) se denominan funciones (término utilizado en C/C++/Java; en

otros lenguajes el mismo concepto se conoce por el término de subrutina, procedimiento o subprograma). Un programa procedimental se divide en funciones (idealmente, al menos, cada función tiene un propósito claramente bien

definido y una interfaz también bien definida a las otras funciones del programa).

La idea de romper un programa en funciones se extiende al agrupamiento de un número determinado de funciones en una entidad más grande llamada módulo (que normalmente se agrupa en un archivo o fichero). El principio

siempre es el mismo: agrupar componentes que ejecutan una lista de instrucciones.

La división de un programa en funciones y módulos es una de las características fundamentales de la programación estructurada y que facilita la lectura y comprensión del programa.

Desde un punto de vista de conceptos prácticos de programación, los lenguajes de computadoras tratan dos conceptos fundamentales: datos y algoritmos. Los datos constituyen la información que utiliza y procesa un programa.

Los algoritmos son los métodos que utiliza el programa (instrucciones paso a paso que conducen a la solución del

programa). La ecuación fundamental de la programación estructurada, debida a Niklaus Wirth es:

Algoritmos + Datos = Programas

La programación estructurada utiliza fundamentalmente instrucciones secuenciales, de selección (if-then,

case) y repetitivas (for, while y do-while) para facilitar la realización de tareas secuenciales, selectivas o de

decisión y repetitivas o iterativas. El diseño del programa estructurado se consigue rompiendo un programa (o un

problema) grande en unidades o tareas más pequeñas y manejables llamadas funciones ( en C, C++ o Java).

15.1.1. Limitaciones de la programación estructurada

Cuando el problema a resolver es complejo, la programación se hace difícil y excesivamente compleja. Las dificultades provienen de que las funciones tienen acceso ilimitado a datos globales y además el paradigma o enfoque procedimental proporcionan un modelo pobre del mundo real.

Desde el punto de vista de un lenguaje procedimental, como C, existen dos tipos de datos: locales y globales. Los

datos locales están ocultos en el interior de la función y se utilizan exclusivamente por la función. Los datos globales son aquellos que pueden ser accedidos por cualquier función del programa.

Accesible sólo por función B

Accesibles, por cualquier función

Accesible sólo por función A

Función A

Variables locales

Función B

Variables locales

Variables globales

Figura 15.1. Datos locales y globales.

P:151

Tipos abstractos de datos, objetos y modelado con UML 2.1 555

En un programa grande existen muchas funciones y muchos datos globales y eso conduce a un número muy

grande de posibles conexiones entre ellos. Lo que dificulta la conceptualización de la estructura del programa y la

modificación del propio programa.

Función Función Función Función

Datos

globales

Datos

globales

Datos

globales

Figura 15.2. Un programa procedimental.

15.1.2. Modelado de objetos del mundo real

La otra limitación de la programación estructurada reside en el hecho de que la separación de los datos y las funciones que manipulan esos datos proporcionan un modelo muy pobre de las cosas y objetos del mundo real. En el mundo real se trata con objetos tales como personas, casas o motocicletas, y tienen a su vez incorporados atributos (datos) y comportamiento (funciones). Los objetos —complejos o no complejos— del mundo real tienen atributos y

comportamiento.

Los atributos o características son las propiedades de los objetos; por ejemplo, para las personas, la estatura, el

color del cabello y de los ojos, la edad, etc.; para un automóvil (coche o carro), la marca, la potencia, el número de

puertas, el precio, etc. Los atributos del mundo real son equivalentes a los datos de un programa y tienen un valor

determinado, 200 metros cuadrados, 20.000 dólares, cinco puertas, etc.

Atributos = Datos

Persona = color del cabello (moreno)

color de los ojos (castaños)

estatura (1,83 m.)

El comportamiento es la acción que realizan los objetos del mundo real en respuesta a un determinado estímulo. Por ejemplo, si se acelera un coche (carro), aumenta su velocidad; si se frena un coche se ralentiza o para. El

comportamiento es similar a una función; la llamada a una función para realizar una tarea determinada, por ejemplo

dibujar un rectángulo o visualizar la nómina de los empleados de una empresa

Comportamiento = Funciones

Por estas razones, ni los datos ni las funciones, por sí mismas, modelan los objetos del mundo real de un modo

eficiente y es la programación orientada a objetos el mejor método para modelar aplicaciones reales. La idea fundamental de la programación orientada a objetos es combinar en una sola entidad tanto los datos como las funciones

que actúan sobre los datos. Tal unidad se denomina objeto. Esta característica permite modelar los objetos del mundo real de un modo mucho más eficiente que utilizando funciones y datos.

P:152

556 Fundamentos de programación

La programación estructurada mejora la claridad, fiabilidad y facilidad de mantenimiento de los programas; sin

embargo, para programas grandes o a gran escala, presentan retos de difícil solución, que pueden ser resueltos

más fácilmente mediante programación orientada a objetos.

15.2. PROGRAMACIÓN ORIENTADA A OBJETOS

Al contrario que el enfoque procedimental que se basa en la interrogante ¿qué hace este programa?, el enfoque

orientado a objetos responde a otro interrogante ¿qué objetos del mundo real puede modelar?

La POO (Programación Orientada a Objetos) se basa en el hecho de que se debe dividir el programa, no en tareas,

sino en modelos de objetos físicos o simulados. Aunque esta idea parece abstracta a primera vista, se vuelve más

clara cuando se consideran objetos físicos en términos de sus clases, componentes, propiedades y comportamiento,

y sus objetos instanciados o creados de las clases.

Si se escribe un programa de computadora en un lenguaje orientado a objetos, se está creando, en su computadora, un modelo de alguna parte del mundo. Las partes que el modelo construye son los objetos que aparecen en el

dominio del problema. Estos objetos deben ser representados en el modelo que se está creando en la computadora.

Los objetos se pueden agrupar en categorías, y una clase describe —de un modo abstracto— todos los objetos

de un tipo o categoría determinada.

Los objetos en C++, o en Java, modelan objetos del problema en el dominio de la aplicación.

Los objetos se crean a partir de las clases. La clase describe el tipo del objeto; los objetos representan instanciaciones individuales de la clase.

La idea fundamental de la orientación a objetos y de los lenguajes que implementan este paradigma de programación es combinar (encapsular) en una única unidad tanto los datos como las funciones que operan (manipulan)

sobre los datos. Esta característica permite modelar los objetos del mundo real de un modo mucho más eficiente que

con funciones y datos. Esta unidad de programación se denomina objeto.

Las funciones de un objeto, se llaman funciones miembro (en C++) o métodos (Java y otros lenguajes de programación), constituyen el único método para acceder a sus datos. Si se desea leer datos de un objeto se llama a una

función miembro del objeto. Se accede a los datos y se devuelve un valor. No se puede acceder a los datos directamente. Los datos están ocultos y se dice que junto con las funciones están encapsulados en una entidad única. La

encapsulación o encapsulamiento de los datos y la ocultación de los datos son conceptos clave en programación

orientada a objetos.

La modificación de los datos de un objeto se realiza a través de una de las funciones miembro de ese objeto que

interactúa con él, y ninguna otra función puede acceder a los datos. Esta propiedad facilita la escritura, depuración

y mantenimiento de un programa.

En un sistema orientado a objetos, un programa se organiza en un conjunto finito de objetos que contienen datos

y operaciones (funciones miembro o método) que se comunican entre sí mediante mensajes (llamadas a funciones

miembro). La estructura de un programa orientado a objetos se muestra en la Figura 15.3.

Las etapas necesarias para modelar un sistema —resolver en consecuencia un problema— empleando orientación

a objetos son:

1. Identificación de los objetos del problema.

2. Agrupamiento en clases (tipos de objetos) de los objetos con características y comportamiento comunes.

3. Identificación de los datos y operaciones de cada una de las clases.

4. Identificación de las relaciones existentes entre las diferentes clases del modelo.

Los objetos encapsulan datos y funciones miembro o métodos que manipulan los datos. Las funciones miembro

también se conocen como métodos —dependiendo de los lenguajes de programación—, por esta razón suelen ser

términos sinónimos. Los elementos dato de un objeto se conocen también como atributos o variables de instancia

P:153

Tipos abstractos de datos, objetos y modelado con UML 2.1 557

(ya que instancia es un objeto específico). La llamada a una función miembro de un objeto se conoce como envío de

un mensaje al objeto.

Objeto

• Funciones miembro o métodos.

• Atributos o variables de instancia (datos).

Envío de un mensaje a un objeto

• Llamada a la función miembro (mensaje).

15.2.1. Objetos

Una forma de reducir la complejidad es, como se verá más en profundidad en los siguientes apartados y capítulos,

la abstracción. Las características y procesos de cualquier sistema se resumen en los aspectos esenciales y más relevantes; de este modo, las características complejas de los sistemas se vuelven más manejables.

En computación, la abstracción es el proceso crucial de representar la información en términos de su interfaz con

el usuario. Es decir, se abstraen las características operacionales esenciales de un problema y expresa su solución en

dichos términos. La abstracción se manifiesta en C++ con el diseño de una clase que implementa la interfaz y que

no es más que un tipo de dato específico.

Un ejemplo de abstracción expresada de diferentes formas según la aplicación a desarrollar, puede ser el término

o clase auto (o bien coche, carro):

• Un auto es la composición o combinación de diferentes partes (motor, cuatro ruedas, 3 o 5 puertas, asientos, etc.).

• Un auto también es un término común que define a tipos diferentes de automóviles; se pueden clasificar por

el fabricante (BMW, Seat, Chevrolet, Toyota...,), por su categoría (o uso) (deportivo, todo-terreno,

sedán, pick-up, limousine, coupé, etc.).

Objeto

Función miembro

(método)

Función miembro

(método)

Datos

Objeto

Función miembro

(método)

Función miembro

(método)

Datos

Objeto

Función miembro

(método)

Función miembro

(método)

Datos

Figura 15.3. Organización típica de un programa orientada a objetos.

P:154

558 Fundamentos de programación

Si los miembros de coches o las diferencias entre coches individuales no son relevantes, se utiliza el término

coche (o carro, en Latinoamérica) y entonces se utilizan expresiones y operaciones tales como: se fabrican coches,

se usan coches para ir de México DF a Guadalajara, se descompone el coche en sus partes...

El objeto es el centro de la programación orientada a objetos. Un objeto es algo que se visualiza, se utiliza y que

juega un papel o un rol. Cuando se programa de modo orientado a objetos se trata de descubrir e implementar los

objetos que juegan un rol en el dominio del problema del programa. La estructura interna y el comportamiento de un

objeto, en consecuencia, no es prioritario durante el modelado del problema. Es importante considerar que un objeto,

tal como un auto juega un rol o papel importante.

Dependiendo del problema, diferentes aspectos de un objeto son significativos. Así, los atributos indican propiedades de los objetos: propietario, marca, año de matriculación, potencia, etc. El objeto también

tiene funciones que actuarán sobre los atributos o datos; matricular, comprar, vender, acelerar, frenar,

etcétera.

Un objeto no tiene que ser necesariamente algo concreto o tangible. Puede ser totalmente abstracto y puede también describir un proceso. Un equipo de baloncesto puede ser considerado como un objeto, los atributos pueden ser

los jugadores, el color de sus camisetas, partidos jugados, tiempo de juego, etc. Las clases con atributos y funciones

miembro permiten gestionar los objetos dentro de los programas.

15.2.2. Tipos abstractos de datos: CLASES

Un progreso importante en la historia de los lenguajes de programación se produjo cuando se comenzó a combinar

juntos diferentes elementos de datos y, por consiguiente, encapsular o empaquetar diferentes propiedades en un tipo

de dato. Estos tipos fueron las estructuras o registros que permiten a una variable contener datos que pertenecen a

las circunstancias representadas por ellas.

Las estructuras representan un modo de abstracción con los programas, concretamente la combinación (o composición) de partes diferentes o elementos (miembros). Así, por ejemplo, una estructura coche constará de miembros

tales como marca, motor, número de matrícula, año de fabricación, etc.

Sin embargo, aunque en las estructuras y registros se pueden almacenar las propiedades individuales de los objetos en los miembros, en la práctica cómo están organizados, no pueden representar qué se puede hacer con ellos

(moverse, acelerar, frenar, etc. en el caso de un coche/carro). Se necesita que las operaciones que forman la interfaz

de un objeto se incorporen también al objeto.

El tipo abstracto de datos (TAD) describe, no sólo los atributos de un objeto sino también su comportamiento

(operaciones o funciones) y, en consecuencia, se puede incluir una descripción de los estados que puede tener el

objeto.

Así, un objeto "equipo de baloncesto" no sólo puede describir a los jugadores, la puntuación, el tiempo

transcurrido, el periodo de juego, etc., sino que también se puede representar operaciones tales como "sustituir

un jugador", "solicitar tiempo muerto", ... , o restricciones tales como, el momento en que comienza 0:00 y en que termina cada cuarto de juego 15:00, o incluso las paradas del tiempo, por lanzamientos de personales.

El término tipo abstracto de dato se consigue en programación orientada a objetos con el término clase. Una

clase es la implementación de un tipo abstracto de dato y describe no sólo los atributos (datos) de un objeto sino

también sus operaciones (comportamiento). Así, la clase carro define que un coche/carro consta de motor, ruedas,

placa de matrícula, etc. y se puede conducir (manejar), acelerar, frenar, etc.

Instancias

Una clase describe un objeto, en la práctica múltiples objetos. En conceptos de programación, una clase es, realmente, un tipo de dato, y se pueden crear, en consecuencia, variables de ese tipo. En programación orientada a

objetos, a estas variables, se las denomina instancias (“instances”), y también por sus sinónimos ejemplares, casos,

etcétera.

Las instancias son la implementación de los objetos descritos en una clase. Estas instancias constan de los datos

o atributos descritos en la clase y se pueden manipular con las operaciones definidas en la propia clase.

En un lenguaje de programación OO, objeto e instancia son términos sinónimos. Así, cuando se declara una variable de tipo Auto, se crea un objeto Auto (una instancia de la clase Auto).

P:155

Tipos abstractos de datos, objetos y modelado con UML 2.1 559

Métodos

En programación orientada a objetos, las operaciones definidas para los objetos, se denominan —como ya se ha comentado— métodos. Cuando se llama a una operación de un objeto se interpreta como el envío de un mensaje a

dicho objeto.

Un programa orientado a objetos se forma enviando mensajes a los objetos, que a su vez producen (envían) más

mensajes a otros objetos. Así, cuando se llama a la operación "conducir" (manejar) para un objeto auto en realidad

lo que se hace es enviar el mensaje "conducir" al objeto auto, que procesa (ejecuta), a continuación, el método

correspondiente.

En la práctica un programa orientado a objetos es una secuencia de operaciones de los objetos que actúan sus

propios datos.

Un objeto es una instancia o ejemplar de una clase (categoría o tipo de datos). Por ejemplo, un alumno y un

profesor, somos instancias de la clase Persona. Un objeto tiene una estructura, como ya se ha comentado. Es

decir, tiene atributos (propiedades) y comportamiento. El comportamiento de un objeto consta de operaciones

que se ejecutan. Los atributos y las operaciones se llaman características del objeto.

Ejemplos

Un objeto de la clase Persona puede tener estos atributos: altura, peso y edad. Cada uno de estos atributos es

único ya que son los valores específicos que tiene cada persona. Se pueden ejecutar estas operaciones: dormir, leer,

escribir, hablar, trabajar, correr, etc. En C++, o en Java, estas operaciones dan lugar a las funciones miembro, o métodos: dormir(), leer(), escribir(), hablar(), trabajar(), correr(), etc. Si tratamos de modelar un sistema académico con profesores y alumnos, éstas y otras operaciones y atributos pertenecerán a dichos objetos y clases.

En el mundo de la orientación a objetos, una clase sirve para otros propósitos que los indicados de clasificación

o categoría. Una clase es una plantilla (tipo de dato) para hacer o construir objetos.

Por ejemplo, una clase Lavadora puede tener los atributos nombreMarca, númeroSerie, capacidad y potencia; y las operaciones encender() - prender -, apagar(), lavar(), aclarar()...



 

  





 





 





  

  

  

Figura 15.4. Notación gráfica de la clase Lavadora en UML.

Un anticipo de reglas de notación en UML 2.0

(En el Capítulo 16 se describe más en detalle la notación UML.)

• El nombre de la clase comienza con una letra mayúscula (Lavadora).

• El nombre de la clase puede tener varias palabras y todas comenzar con mayúsculas, por ejemplo PaginaWeb.

• El nombre de una característica (atributo u operación) comienza con una letra minúscula (estatura).

• El nombre de una característica puede constar a su vez de dos palabras, en este caso la primera comienza con minúscula y la segunda con mayúscula (nombreMarca).

• Un par de paréntesis sigue al nombre de una operación; por ejemplo, limpiar ().

P:156

560 Fundamentos de programación

Esta notación facilita el añadido o supresión de características por lo cual representan gráficamente con mayor

precisión un modelo del mundo real; por ejemplo, a la clase Lavadora se le podría añadir velocidadMotor,

volumen, aceptarDetergente(), ponerProgrma(), etc.

15.3. MODELADO E IDENTIFICACIÓN DE OBJETOS

Un objeto en software es una entidad individual de un sistema que guarda una relación directa con los objetos del

mundo real. La correspondencia entre objetos de programación y objetos del mundo real es el resultado práctico de

combinar atributos y operaciones, o datos y funciones. Un objeto tiene un estado, un comportamiento y una identidad.

Estado

Conjunto de valores de todos los atributos de un objeto en un instante de tiempo determinado. El estado de un objeto viene determinado por los valores que toman sus datos o atributos. Estos valores han de cumplir siempre las restricciones (invariantes de clase, para objetos pertenecientes a la misma clase) que se hayan impuesto. El estado de

un objeto tiene un carácter dinámico que evo luciona con el tiempo, con independencia de que ciertos elementos del

objeto puedan permanecer constantes.

Comportamiento

Conjunto de operaciones que se pueden realizar sobre un objeto. Las operaciones pueden ser de observación del estado interno del objeto, o bien de modificación de dicho estado. El estado de un objeto puede evolucionar en función

de la aplicación de sus operaciones. Estas operaciones se realizan tras la recepción de un mensaje o estímulo externo

enviado por otro objeto. Las interacciones entre los objetos se representan mediante diagramas de objetos. En UML

se representarán por enlaces en ambas direcciones.

Identidad

Permite diferenciar los objetos de modo no ambiguo independientemente de su estado. Es posible distinguir dos objetos en los cuáles todos sus atributos sean iguales. Cada objeto posee su propia identidad de manera implícita. Cada

objeto ocupa su propia posición en la memoria de la computadora.

Mensaje

Objeto1 Datos Datos

Objeto2

Figura 15.5. Comunicación entre objetos por mensajes.

En un sistema orientado a objetos los programas se organizan en conjuntos finitos que contienen atributos (datos)

y operaciones (funciones) y que se comunican entre sí mediante mensajes. Los pasos típicos en el modelado de un

sistema orientado a objetos son:

1. Identificar los objetos que forman parte del modelo.

2. Agrupar en clases todos aquellos objetos que tengan características y comportamientos comunes.

3. Identificar los atributos y las operaciones de cada clase.

4. Identificar las relaciones existentes entre las clases.

Cuando se diseña un problema en un lenguaje orientado a objetos, se debe pensar en dividir dicho problema en

objetos, o dicho de otro modo, es preciso identificar y seleccionar los objetos del dominio del problema de modo que

exista una correspondencia entre los objetos desde el punto de vista de programación y los objetos del mundo real.

¿Qué tipos de cosas se convierten en objetos en los programas orientados a objetos? La realidad es que la gama

de casos es infinita y sólo dependen de las características del problema a resolver y la imaginación del programador.

Algunos casos típicos son:

P:157

Tipos abstractos de datos, objetos y modelado con UML 2.1 561

Personas Partes de una computadora Estructuras de datos

Empleados Pantalla Pilas

Estudiantes Unidades de CDs, DVDs Colas

Clientes Impresora Listas enlazadas

Profesores Teclado Árboles

Edición de revistas Objetos físicos Archivos de datos

Artículos Autos Diccionario

Nombres de autor Mesas Inventario

Volumen Carros Listas

Número Ventanas Ficheros

Fecha de publicación Farolas Almacén de datos

La correspondencia entre objetos de programación y objetos del mundo real se muestra en una combinación

entre datos y funciones.

15.4. PROPIEDADES FUNDAMENTALES DE ORIENTACIÓN A OBJETOS

Los conceptos fundamentales de orientación a objetos que a su vez se constituyen en reglas de diseño en un lenguaje de programación orientado a objetos son: abstracción, herencia (generalización), encapsulamiento, ocultación de

datos, polimorfismo y reutilización. Otras propiedades importantes son: envío de mensajes y diferentes tipos de relaciones tales como, asociaciones y agregaciones (véase Capítulo 17).

15.4.1. Abstracción

La abstracción es la propiedad que considera los aspectos más significativos o notables de un problema y expresa

una solución en esos términos. En computación, la abstracción es la etapa crucial de representación de la información

en términos de la interfaz con el usuario. La abstracción se representa con un tipo definido por el usuario, con el

diseño de una clase que implementa la interfaz correspondiente. Una clase es un elemento en C++ o en Java, que

traduce una abstracción a un tipo definido por el usuario. Combina representación de datos y métodos para manipular esos datos en un paquete.

La abstracción posee diversos grados denominados niveles de abstracción que ayudan a estructurar la complejidad

intrínseca que poseen los sistemas del mundo real. En el análisis de un sistema hay que concentrarse en ¿qué hace?

y no en ¿cómo lo hace?

El principio de la abstracción es más fácil de entender con una analogía del mundo real. Por ejemplo, una televisión es una aparato electrodoméstico que se encuentra en todos los hogares. Seguramente, estará familiarizado con

sus características y su manejo manual o con el mando a distancia: encender (prender), apagar, cambiar de canal,

ajustar el volumen, cambiar el brillo..., y añadir componentes externos como altavoces, grabadoras de CDs, reproductoras de DVDs, conexión de un modem para Internet, etc. Sin embargo, ¿sabe usted cómo funciona internamente?, ¿conoce cómo recibe la señal por la antena, por cable, por satélite, traduce la señal y la visualiza en pantalla?

Normalmente sucederá que no sepamos cómo funciona el aparato de televisión, pero sí sabemos cómo utilizarlo. Esta

característica se debe a que la televisión separa claramente su implementación interna de su interfaz externa (el cuadro de mandos de su aparato o su mando a distancia). Actuamos con la televisión a través de su interfaz: los botones

de alimentación, de cambio de canales, control de volumen, etc. No conocemos el tipo de tecnología que utiliza, el

método de generar la imagen en la pantalla o cómo funciona internamente, es decir su implementación, ya que ello

no afecta a su interfaz.

15.4.2. La abstracción en el software

El principio de abstracción es similar en el software. Se puede utilizar código sin conocimiento de la implementación

fundamental. En C++, por ejemplo, se puede hacer una llamada a la función sqrt(), declarada en el archivo de

cabecera <math.h>, que puede utilizar sin necesidad de conocer la implementación del algoritmo real que calcular

P:158

562 Fundamentos de programación

la raíz cuadrada. De hecho, la implementación fundamental del cálculo de la raíz cuadrada puede cambiar en las

diferentes versiones de la biblioteca, mientras que la interfaz permanece la misma.

También se puede aplicar el principio de abstracción a las clases. Así, se puede utilizar el objeto cout de la clase ostream para fluir (enviar) datos a la salida estándar, en C++, tal como

cout << " En un lugar de Cazorla \

";

o en pseudocódigo

escribir "En un lugar de Cazorla" <fin de línea>

En la línea anterior, se utiliza la interfaz documentada del operador de inserción cout con una cadena, pero no

se necesita comprender cómo cout administra visualizar el texto en la interfaz del usuario. Sólo necesita conocer

la interfaz pública. Así, la implementación fundamental de cout es libre de cambiar, mientras que el comportamiento

expuesto y la interfaz permanecen iguales.

La abstracción es el principio fundamental que se encuentra tras la reutilización. Sólo se puede reutilizar un componente si en él se ha abstraído la esencia de un conjunto de elementos del confuso mundo real que aparecen una y

otra vez, con ligeras variantes en sistemas diferentes.

15.4.3. Encapsulamiento y ocultación de datos

La encapsulación o encapsulamiento, significa reunir en una cierta estructura a todos los elementos que a un cierto nivel de abstracción se pueden considerar pertenecientes a una misma entidad, y es el proceso de agrupamiento de

datos y operaciones relacionadas bajo una misma unidad de programación, lo que permite aumentar la cohesión de

los componentes del sistema.

En este caso, los objetos que poseen las mismas características y comportamiento se agrupan en clases que no

son más que unidades de programación que encapsulan datos y operaciones.

La encapsulación oculta lo que hace un objeto de lo que hacen otros objetos y del mundo exterior, por lo que se

denomina también ocultación de datos.

Un objeto tiene que presentar “una cara” al mundo exterior de modo que se puedan iniciar esas operaciones. El

aparato de TV tiene un conjunto de botones, bien en la propia TV o incorporados en un mando a distancia. Una máquina lavadora tiene un conjunto de mandos e indicadores que establecen la temperatura y el nivel del agua. Los

botones de la TV y las marchas de la máquina lavadora constituyen la comunicación con el mundo exterior, las interfaces.

En esencia, la interfaz de una clase representa un “contrato” de prestación de servicios entre ella y los demás

componentes del sistema. De este modo, los clientes de un componente sólo necesitan conocer los servicios que éste

ofrece y no cómo están implementados internamente.

Por consiguiente, se puede modificar la implementación de una clase sin afectar a las restantes relacionadas con

ella. En general, esto implica mantener el contrato y se puede modificar la implementación de una clase sin afectar

a las restantes clases relacionadas con ella; sólo es preciso mantener el contrato. Existe una separación de la interfaz

y de la implementación. La interfaz pública establece qué se puede hacer con el objeto; de hecho, la clase actúa como

una “caja negra”. La interfaz pública es estable, pero la implementación se puede modificar.

15.4.4 Herencia

El concepto de clase conduce al concepto de herencia. En la vida diaria se utiliza el concepto de clases que se dividen a su vez en subclases. La clase animal se divide en mamíferos, anfibios, insectos, pájaros, etc. La

clase vehículo se divide en autos, coches carros, camiones, buses, motos, etc. La clase electrodoméstico se divide en lavadora, frigorífico, tostadora, microondas, etc.

La idea principal de estos tipos de divisiones reside en el hecho de que cada subclase comparte características

con la clase de la cual se deriva. Los autos, camiones, buses, motos..., tienen motor, ruedas y frenos. Además de estas características compartidas, cada subclase tiene sus propias características. Los autos, por ejemplo, pueden tener

maletero, cinco asientos; los camiones cabina y caja para transportar carga, etc.

La clase principal de la que derivan las restantes se denomina clase base —en C++ —, clase padre o superclase

y las subclases, también se denominan clases derivadas —en C++ —.

P:159

Tipos abstractos de datos, objetos y modelado con UML 2.1 563

Tostadora Horno

de microondas Lavadora Frigorífico

Electrodoméstico

Figura 15.6. Herencia simple.

Las clases modelan el hecho de que el mundo real contiene objetos con propiedades (atributos) y comportamiento. La herencia modela el hecho de que estos objetos tienden a organizarse en jerarquías. Esta jerarquía desde el

punto de vista del modelado se denomina relación de generalización o es-un (is-a). En programación orientada

a objetos, la relación de generalización se denomina herencia. Cada clase derivada hereda las características de la

clase base y además cada clase derivada añade sus propias características (atributos y operaciones). Las clases bases

pueden a su vez ser también subclases o clases derivadas de otras superclases o clases base.

Así, el programador puede definir una clase Animal que encapsula todas las propiedades o atributos (altura,

peso, número de patas, etc.) y el comportamiento u operaciones (comer, dormir, andar) que pertenecen a cada

animal. Los animales específicos como un Mono, Jirafa, Canguro o Pingüino tienen a su vez cada uno de ellos

características propias.

Mono Jirafa Canguro Pingüino

Animal

Figura 15.7. Herencia de la clase Animal.

Las técnicas de herencia se representan con la citada relación es-un. Así, se dice que un Mono es-un Animal que

tiene características propias: puede subir a los árboles, saltar de un árbol a otro, etc.; además tiene en común con la

Jirafa, el Canguro y el Pingüino, las características propias de cualquier animal (comer, beber, corer, dormer,…).

15.4.5. Reutilización o reusabilidad

Otra propiedad fundamental de la programación orientada a objetos es la reutilización o reusabilidad. Este concepto significa que una vez se ha creado, escrito y depurado una clase, se puede poner a disposición de otros programadores. El concepto es similar al uso de las bibliotecas de funciones en un lenguaje de programación procedimental como C.

El concepto de herencia en C++ o Java, proporciona una ampliación o extensión importante a la idea de reusabilidad. Una clase existente se puede ampliar añadiéndola nuevas características (atributos y operaciones). Esta acción

se realiza derivando una nueva clase de la clase existente. La nueva clase hereda las características de la clase base;

pero podrá añadirle nuevas características.

Reutilización de código

La facilidad de reutilizar o reusar el software existente es uno de los grandes beneficios de la POO. De este modo en

una empresa de software se pueden reutilizar clases diseñadas en un proyecto en un nuevo proyecto con la consiguiente mejora de la productividad, al sacarle partido a la inversión realizada en el diseño de la clase primitiva.

P:160

564 Fundamentos de programación

¿En esencia cuáles son las ventajas de la herencia? Primero, reducción de código; las otras propiedades comunes

de varias clases sólo necesitan ser implementadas una vez y sólo necesitan modificarse una vez si es necesario. Segundo, está soportado el concepto de abstracción de la funcionalidad común

La facilidad con la que el software existente se puede reutilizar es una propiedad muy importante de la POO. La

idea de usar código existente no es nueva en programación, ni lógicamente en C++ o Java. Cada vez que se imprime

algo con cout se está reutilizando código. No se escribe el código de salida a pantalla para visualizar los datos sino

que se utiliza el flujo ostream existente para realizar el trabajo. Muchas veces los programadores no tienen en cuenta la ventaja de utilizar el código disponible y, como se suele decir en la jerga de la programación, “inventan la rueda” cada día, o al menos en cada proyecto.

Por ejemplo, supongamos que usted desea ejecutar un planificador de un sistema operativo. El planificador (scheduler) es un componente del SO responsable de planificar procesos (cuáles se deben ejecutar y durante qué tiempo).

Para realizar esta planificación se suele recurrir a implementaciones basadas en prioridades, para lo cual se requiere

una estructura de datos denominada cola de prioridades que almacena los procesos a la espera de que se vayan ejecutando en función de su prioridad. Existen en C++ dos métodos: (1) escribir el código correspondiente a una cola

de prioridades; (2) la biblioteca estándar de plantillas STL incorpora un contenedor denominado priorty-queue (cola

de prioridad) que se puede utilizar para almacenar objetos de cualquier tipo. Lo más sensato es incorporar en su programa el contenedor priority-queue de la biblioteca STL en un diseño del planificador, en lugar de reescribrir su

propia cola de prioridad.

Reescritura de código reusable

Es importante en el diseño de un código la escritura de código reusable. Debe diseñar sus programas de modo que

pueda reutilizar sus clases, sus algoritmos y sus estructuras de datos. Tanto usted como sus colegas en el proyecto en

que trabaje deben poder utilizar los componentes anteriores, tanto en el proyecto actual como en futuros proyectos.

En general, se debe evitar diseñar código en exceso o específico para casos puntuales; siempre que sea posible reutilice código existente.

Una técnica del lenguaje C++ para escribir código de propósito general es utilizar plantillas (templates). En lugar

de escribir, por ejemplo, una estructura de datos Pila para tratar números reales, etc. es preferible diseñar una plantilla o tipo genérico Pila que le sirva para cualquier tipo de dato a procesar.

15.4.6. Polimorfismo

El polimorfismo es la propiedad que permite a una operación (función) tener el mismo nombre en clases diferentes

y que actúe de modo diferente en cada una de ellas. Esta propiedad es intrínseca a la vida ordinaria ya que una misma operación puede realizar diferentes acciones dependiendo del objeto sobre el que se aplique. Así, por ejemplo, se

puede abrir una puerta, abrir una ventana, abrir un libro, abrir un periódico, abrir una cuenta corriente en un banco,

abrir una conversación, abrir un congreso, etc. En cada caso se realiza una operación diferente. En orientación a objetos, cada clase “conoce” cómo realizar esa operación.

En la práctica, el polimorfismo significa la capacidad de una operación de ser interpretada sólo por el propio

objeto que lo invoca. Desde un punto de vista práctico de ejecución del programa, el polimorfismo se realiza en

tiempo de ejecución ya que durante la compilación no se conoce qué tipo de objeto y por consiguiente qué operación

ha sido invocada.

En la vida diaria hay numerosos ejemplos de polimorfismo. En un taller de reparaciones de automóviles existen

numerosos automóviles de marcas diferentes, de modelos diferentes, de potencias diferentes, de carburantes diferentes, etc. Constituyen una clase o colección heterogénea de carros (coches). Supongamos que se ha de realizar una

operación típica “cambiar los frenos del carro”. La operación a realizar es la misma, incluye los mismos principios

de trabajo, sin embargo, dependiendo del coche, en particular, la operación será muy diferente, incluirá diferentes

acciones en cada caso.

La propiedad de polimormismo, en esencia, es aquella en que una operación tiene el mismo nombre en diferentes

clases, pero se ejecuta de diferentes formas en cada clase

El polimorfismo es importante tanto en el modelado de sistemas como en el desarrollo de software. En el modelado porque el uso de palabras iguales tiene comportamientos distintos, según el problema a resolver. En software,

porque el polimorfismo toma ventaja de la propiedad de la herencia.

En el caso de operaciones en C++ o Java, el polimorfismo permite que un objeto determine en tiempo de ejecución la operación a realizar. Supongamos, por ejemplo, que se trata de realizar un diseño gráfico para representar

P:161

Tipos abstractos de datos, objetos y modelado con UML 2.1 565

figuras geométricas, tales como triángulo, rectángulo y circunferencia, y que se desea calcular la superficie o el perímetro (longitud) de cada figura. Cada figura tiene un método u operación distinta para calcular su perímetro y su

superficie. El polimorfismo permite definir una única función calcularSuperficie, cuya implementación es diferente en la clase Triángulo, Rectángulo o Circunferencia, y cuando se selecciona un objeto específico en

tiempo de ejecución, la función que se ejecuta es la correspondiente al objeto específico de la clase seleccionada.

En C++ y Java existe otra propiedad importante derivada del polimorfismo y es la sobrecarga de operadores

y funciones, que es un tipo especial de polimorfismo. La sobrecarga básica de operadores existe siempre. El operador + sirve para sumar números enteros o reales, pero si desea sumar números complejos deberá sobrecargar el operador + para que permita realizar esta suma.

El uso de operadores o funciones de forma diferente, dependiendo de los objetos sobre los que están actuando se

denomina polimorfismo (una cosa con diferentes formas). Sin embargo, cuando un operador existente, tal como + o =,

se le permite la posibilidad de operar con diferentes tipos de datos, se dice que dicho operador está sobrecargado. La

sobrecarga es un tipo especial de polimorfismo y una característica sobresaliente de los lenguajes de programación

orientada a objetos.

15.5. MODELADO DE APLICACIONES: UML

El Lenguaje Unificado de Modelado (UML, Unified Model Language), es el lenguaje estándar de modelado para

desarrollo de sistemas y de software. UML se ha convertido de facto en el estándar para modelado de aplicaciones

software y ha crecido su popularidad en el modelado de otros dominios. Tiene una gran aplicación en la representación y modelado de la información que se utiliza en las fases de análisis y diseño. En diseño de sistemas, se modela

por una importante razón: gestionar la complejidad.

Un modelo es una abstracción de cosas reales. Cuando se modela un sistema, se realiza una abstracción ignorando los detalles que sean irrelevantes. El modelo es una simplificación del sistema real. Con un lenguaje firmal de

modelado, el lenguaje es abstracto aunque tan preciso como un lenguaje de programación. Esta precisión permite que

un lenguaje sea legible por la máquina, de modo que pueda ser interpretado, ejecutado y transformado entre sistemas.

Para modelar un sistema de modo eficiente, se necesita una cosa muy importante: un lenguaje que pueda describir el modelo. ¿Qué es UML? UML es un lenguaje. Esto significa que tiene tanto sintaxis como semántica y se

compone de: pseudocódigo, código real, dibujos, programas, descripciones... Los elementos que constituyen un lenguaje de modelado se denominan notación.

El bloque básico de construcción de UML es un diagrama. Existen tipos diferentes, algunos con propósitos muy

específicos (diagramas de tiempo) y algunos con usos más genéricos (diagramas de clases).

Un lenguaje de modelado puede ser cualquier cosa que contiene una notación (un medio de expresar el modelo)

y una descripción de lo que significa esa notación (un meta-modelo). Aunque existen diferentes enfoques y visiones

de modelado, UML tiene un gran número de ventajas que lo convierten en un lenguaje idóneo para un gran número

de aplicaciones tales como:

• Diseño de software.

• Software de comunicaciones.

• Proceso de negocios.

• Captura de detalles acerca de un sistema, proceso u organización en análisis de requisitos.

• Documentación de un sistema, proceso o sistema existente.

UML se ha aplicado y se sigue aplicando en un sinfín de dominios, tales como:

• Banca.

• Salud.

• Defensa.

• Computación distribuida.

• Sistemas empotrados.

• Sistemas en tiempo real.

• Etcétera.

El bloque básico fundamental de UML es un diagrama. Existen diferentes tipos, algunos con propósitos muy

específicos (diagramas de tiempos) y algunos con propósitos más genéricos (diagramas de clase).

P:162

566 Fundamentos de programación

15.5.1. Lenguaje de modelado

Un modelo es una simplificación de la realidad que se consigue mediante una abstracción. El modelado de un sistema pretende capturar las partes fundamentales o esenciales de un sistema. El modelado se representa mediante una

notación gráfica.

Un modelo se expresa en un lenguaje de modelado y consta de notación —símbolos utilizados en los modelos— y un conjunto de reglas que instruyen cómo utilizarlas. Las reglas son sintácticas, semánticas y pragmáticas.

• Sintaxis, nos indica cómo se utilizan los símbolos para representar elementos y cómo se combinan los símbolos en el lenguaje de modelado. La sintaxis es comparable con las palabras del lenguaje natural; es importante

conocer cómo se escriben correctamente y cómo poner los elementos para formar una sentencia.

• Semántica, las reglas semánticas explican lo que significa cada símbolo y cómo se deben interpretar, bien por

sí misma o en el contexto de otros símbolos.

• Pragmática, las reglas de pragmática definen las intenciones de los símbolos a través de los cuáles se consigue

el propósito de un modelo y se hace comprensible para los demás. Esta característica se corresponde en lenguaje natural con las reglas de construcción de frases que son claras y comprensibles. Para utilizar bien un

lenguaje de modelado es necesario aprender estas reglas.

Un modelo UML tiene dos características muy importantes:

• Estructura estática: describe los tipos de objetos más importantes para modelar el sistema.

• Comportamiento dinámico: describe los ciclos de vida de los objetos y cómo interactúan entre sí para conseguir la funcionalidad del sistema requerida.

Estas dos características están estrechamente relacionadas entre sí.

15.5.2. ¿Qué es un lenguaje de modelado?

UML es el lenguaje de modelado estándar para desarrollo de software y de sistemas. Algunas preguntas importantes,

a responder, para los desarrolladores son: ¿Por qué UML es unificado? ¿Qué se puede modelar? ¿Por qué UML es

un lenguaje?

El diseño de sistemas de media y gran complejidad suele ser difícil. Desde una simple aplicación de escritorio/

oficina hasta un sistema para la web o para gestión de relación con clientes es una gran empresa, entraña construir

centenares —o millares— de componentes de software y hardware. En realidad, la principal razón para modelar un

diseño de sistemas es gestionar (administrar) la complejidad.

Un modelo es una abstracción de cosas del mundo real (tangibles o intangibles). El modelo es una simplificación

del sistema real, de modo que facilite el diseño y la viabilidad de un sistema y que pueda ser comprendido, evaluado,

analizado y criticado. En la práctica un lenguaje formal de modelado puede ser tan preciso como un lenguaje de

programación. Esta precisión puede permitir que un lenguaje sea legible por la máquina, y pueda ser interpretado,

ejecutado y transformado entre sistemas.

Para modelar eficazmente un sistema se necesita una cosa muy importante: un lenguaje que pueda describir con

rigor el modelo.

Un lenguaje de modelado se puede construir con pseudocódigo, código real, dibujos, diagramas, descripciones

de texto, etc. Los elementos que constituyen un lenguaje de modelado se llaman notación.

En general, un modelo UML se compone de uno o más diagramas. Un diagrama gráfico representa cosas y relaciones entre las cosas. Estas cosas pueden ser representaciones de objetos del mundo real, construcciones de software o una descripción del comportamientos de algún otro objeto. En la práctica cada diagrama representa una vista

de la cosa u objeto a modelar.

UML es un lenguaje de modelado visual para desarrollo de sistemas. La característica de extensibilidad hace

que UML se pueda emplear en aplicaciones de todo tipo, aunque su fuerza y la razón por la que fue creado es

modelar sistemas de software orientado a objetos dentro de áreas tales como programación y la ingeniería de software.

UML como lenguaje de modelado visual es independiente del lenguaje de implementación, de modo que los diseños realizados usando UML se pueden implementar en cualquier lenguaje que soporte las características de UML,

esencialmente lenguajes orientados a objetos com C++, C# o Java.

P:163

Tipos abstractos de datos, objetos y modelado con UML 2.1 567

Un lenguaje de modelado puede ser todo aquel que contenga una notación (un medio de expresar el modelo) y

una descripción de lo que significa esa notación (un metamodelo).

¿Qué ventajas aporta UML a otros métodos, lenguajes o sistemas de modelado? Las principales ventajas de UML

son [Miles, Hamilton 06]:

• Es un lenguaje formal, cada elemento del lenguaje está rigurosamente definido.

• Es conciso.

• Es comprensible, completo, describe todos los aspectos importantes de un sistema.

• Es escalable, sirve para gestionar grandes proyectos pero también pequeños proyectos.

• Está construido sobre la filosofía de “lecciones aprendidas”, ya que recogió lo mejor de 3 métodos ya muy

probados y desde su primer borrador, en 1994, ha incorporado las mejores prácticas de la comunidad de orientación a objetos.

• Es un estándar, ya que está avalado por la OMG, organización con difusión y reconocimiento mundial.

15.6. DISEÑO DE SOFTWARE CON UML

Cuando se aplica UML al diseño de software, UML trata de llevar la idea original o la abstracción del problema a

una parte o bloque de software y su implementación. UML proporciona un medio para capturar y examinar requisitos a nivel de requisitos (diagramas de caso de uso), un concepto muy importante pero, a veces, difícil de comprensión para un programador novel. Existen diagramas para capturar cuáles son las partes del software que realizan

ciertos requisitos (diagramas de colaboración). Otros diagramas sirven para capturar exactamente cómo realizan sus

requisitos esas partes del sistema (diagramas de secuencia y diagramas de cartas de estado). Por último, hay diagramas para mostrar todo lo que se acopla y ejecuta de modo conjunto (diagramas de componentes y diagramas de

cartas y de despliegue). UML divide los diagramas en dos categorías: diagramas estructurales y diagramas de comportamientos.

Los diagramas estructurados se utilizan para capturar la organización física de las cosas del sistema, por ejemplo,

cómo se relacionan unos objetos con otros. Los diagramas estructurados o estructurales, son:

• Diagramas de clases o de estructuras.

• Diagramas de componentes.

• Diagramas de estructuras compuestas.

• Diagramas de despliegue.

• Diagramas de objetos.

Los diagramas de comportamiento se centran en el comportamiento de los elementos de un sistema. Un uso típico de los diagramas de comportamiento son:

• Diagramas de actividad.

• Diagramas de comunicación.

• Diagramas de interacción.

• Diagramas de secuencia.

• Diagramas de máquinas de estado.

• Diagramas de tiempo.

• Diagramas de caso de uso.

UML es un lenguaje con una sintaxis y una semántica (vocabulario y reglas) que permiten una comunicación.

UML tiene un amplio vocabulario para capturar el comportamiento y los flujos de procesos. Los diagramas de

actividad y las cartas de estado se pueden utilizar para capturar procesos de negocios que implican a personas, grupos

internos o incluso organizaciones completas. UML 2.1, la última versión de UML, tiene una notación que ayuda

considerablemente a modelar fronteras geográficas, responsabilidades del trabajador, transacciones complejas, etc.

Es importante considerar que UML no es un proceso de software. Esto significa que se utilizará UML dentro de

un proceso de software pero está concebido, claramente, para ser parte de un enfoque de desarrollo iterativo.

P:164

568 Fundamentos de programación

15.6.1. Desarrollo de software orientado a objetos con UML

UML tiene sus orígenes en la orientación a objetos. Por esta razón se puede definir UML como: “un lenguaje de

modelado orientado a objetos para desarrollo de sistemas de software modernos”. Al ser un lenguaje de modelado

orientado a objetos, todos los elementos y diagramas en UML se basan en el paradigma orientado a objetos. El desarrollo orientado a objetos se centra en el mundo real y resuelve los problemas a través de la interpretación de “objetos” que representan los elementos tangibles de un sistema.

La idea fundamental de UML es modelar software y sistemas como colecciones de objetos que interactúan entre

sí. Esta idea se adapta muy bien al desarrollo de software y a los lenguajes orientados a objetos, así como a numerosas aplicaciones, tales como los procesos de negocios.

15.6.2. Especificaciones de UML

UML es un conjunto de especificaciones de OMG. UML 2 está distribuido en cuatro especificaciones: la Especificación de Intercambio de Diagramas, la Infraestructura UML, la Superestructura UML, y el Lenguaje de Restricciones

de objetos OCL (Object Constraint Language). La documentación completa de estas cuatro especificaciones está

disponible en el sitio web oficial OMG (www.omg.org). Si desea profundizar en UML le recomendamos visite este

sitio y consulte la amplia bibliografía que incluimos en el apéndice de bibliografía y en la web general del libro.

Por otra parte, los modelos UML tienen al menos dos dimensiones: una dimensión gráfica para visualizar el modelo usando diagramas e iconos (notaciones) y otra dimensión con texto que describe las especificaciones de distintos elementos de modelado.

La especificación o lenguaje OCL define un lenguaje para escritura de restricciones y expresiones de los elementos del modelo.

15.7. HISTORIA DE UML

En la primera mitad de los noventa, los lenguajes de modelado que imperaban eran: OMT-21

(Object Modeling

Techniquel) creado por James Rumbaugh, OOSE (Object Oriented Software Engineering) creado por Ivan Jacobson

y Booch’932

cuyo creador era Grady Booch. Existían otros métodos que fueron muy utilizados: Coad/Yourdon y

Fusion, entre otros. Fusion de Coleman constituyó un primer intento de unificación de métodos, pero al no contar

con los restantes métodos, pese a ser apoyado por Hewlett-Packard en algún momento, no llegó a triunfar.2

Grady Booch, propietario de Rational llamó a James Rumbaugh y formaron equipo en una nueva Rational Corporation con el objetivo de fusionar sus dos métodos. En octubre de 1994, Grady Booch y James Rumbaugh comenzaron a trabajar en la unificación de ámbos métodos produciendo una versión borrador llamada O,8 y de nombre

“Unified Method”3

. Anteriormente, a partir de 1995, Jacobson se unió al tandem y formaron un equipo que se llegó

a conocer como “los tres amigos”.

El primer fruto de su trabajo colectivo se lanzó en enero de 1997 y fue presentado como versión 1.0 de UML.

La gran ventaja de UML es que fue recogiendo aportaciones de los grandes gurús de objetos; David Hasel con sus

diagramas de estado; partes de la notación de Fusion, el criterio de responsabilidad-colaboración y sus diagramas de

Rebeca Wirfs-Brock y el trabajo de patrones y documentación de Gamma-Helm-Johnson-Ulissides.

En 1997, OMG aceptó UML cómo estándar (versión 1.1) y nació el primer lenguaje de modelado visual orientado a objetos como estándar abierto de la industria. Desde entonces han desaparecido, prácticamente, todas las metodologías y UML se ha convertido en el estándar de la industria del software y de muchas otras industrias que requieren el uso de modelos. En 1998, OMG lanzó dos revisiones más, 1.2 y 1.3. En el año 2000 se presentó UML 1.4

con la importante aportación de la semántica de acción, que describe el comportamiento de un conjunto de acciones

permitidas que se pueden implementar mediante lenguajes de acción. La versión 1.5 siguió a las ya citadas.

En 2004 finalizó la versión 2.0 con su especificación completa. UML 2 es ya un lenguaje de modelado muy maduro.

UML 2 ha incorporado numerosas mejoras a UML 1.x. Los cambios más importantes se han realizado en

el metamodelo (generador de modelos) conservando los principios fundamentales y evolutivos de las últimas ver1, 2 Los libros originales de OMT y Booch’ 93 fueron traducidos por un equipo de profesores universitarios dirigidos por el autor de este libro. 3

Todavía conservo el borrador de una versión —en papel— que fue presentada por James Rumbaugh, entre otras ciudades, en un hotel en

Madrid... invitado por su editorial Prentice-Hall.

P:165

Tipos abstractos de datos, objetos y modelado con UML 2.1 569

siones. Los diseñadores de UML 2.0 —ya no sólo los tres creadores originales, sino una inmensa pléyade que

ha contribuido con infinitas aportaciones— han tenido mucho cuidado en asegurar que UML 2.0 fuera totalmente

compatible con las versiones anteriores para que los usuarios de versiones anteriores no tuvieran problemas de

adaptación.

La versión UML 2.0 ha evolucionado para soportar los nuevos retos a los cuáles se enfrentan el software y los

nuevos desarrolladores de modelos, de modo que los objetivos, que se plantearon hace más de una década los tres

amigos para unificar los diferentes métodos que se utilizaban en diseño de software, han aumentado y se han convertido en un lenguaje unificado de modelado que está preparado y adaptado para continuar siendo el lenguaje estándar utilizado en innumerables tareas diferentes, implicadas en el diseño del software.

En la dirección de Internet //en.wikipeida-org/wiki/Unified_Modeling_Language de la enciclopedia

virtual Wikipedia encontrará una definición extensa de UML normalmente, actualizada y con un gran número de

referencias, enlaces, software, documentación y bibliografía de UML. Igualmente en la página oficial de OMG,

www.omg.org, encontrará la información más actualizada, así como especificaciones y manuales de todas las versiones disponibles.

15.7.1. El futuro de UML 2.1

Dos nuevos enfoques están apareciendo en la segunda mitad de la primera década del siglo XXI: (1) MDA, arquitectura controlada por modelos (Model Driver Architecture, MDA, www.omg.org/mda); (2) PMI, modelo independiente de la plataforma (Platform Specific Model).

MDA es un nuevo enfoque para desarrollo de software basado en modelos. Los fundamentos de este enfoque

residen en la producción de software ejecutable. MDA requiere un metamodelo estructurado formalmente e interpretable para realizar su transformación, y este nivel de metamodelo lo proporciona UML 2.0.

Las aportaciones a UML siguen proliferando y ya existen revisiones distintas de la 2.0. La versión actual disponible, que data de agosto de 2007, es la 2.1.1. La versión 2.2 está actualmente en desarrollo, pero OMG todavía no

ha iniciado el proceso de estandarización y está centrada en la versión actual, UML 2.1.

15.8. TERMINOLOGÍA DE ORIENTACIÓN A OBJETOS

Los lenguajes de programación orientados a objetos utilizados en la actualidad son numerosos y aunque la mayoría

siguen criterios de terminología universales puede haber algunas diferencias relativas a su consideración de puros

(Smalltalk, Eiffel,…) e híbridos (Object Pascal, VB.NET, C++, Java, C#,…). La Tabla 15.1 sintetiza la terminología

utilizada en los manuales de programación de cada respectivo lenguaje.

Tabla 15.1. Terminología de orientación a objetos en diferentes lenguajes de programación

CONCEPTO Object

Pascal VB. NET C++ Java C# Smalltalk Eiffel

Objeto Objeto Objeto Objeto Objeto Objeto Objeto Objeto

Clase Tipo-Objeto Clase Clase Clase Clase Clase Clase

Método

(function miembro)

Método Método Función

Miembro

Método Método Método Rutina

Mensaje Mensaje Mensaje Mensaje Mensaje Mensaje Mensaje Aplicación

Herencia Herencia Herencia Herencia Herencia Herencia Herencia Herencia

Superclase Clase

Base

Clase

Base

Superclase Clase

Base

Superclase Ascendiente

Subclase Descendiente Clase

Derivada

Clase

Derivada

Subclase Clase

Derivada

Subclase Descendiente

P:166

570 Fundamentos de programación

CONCEPTOS CLAVE

• Abstracción.

• ADT.

• Atributos.

• Clase.

• Clase base.

• Clase derivada.

• Comportamiento.

• Comunicación entre objetos.

• Encapsulamiento.

• Estado.

• Función miembro.

• Herencia.

• Herencia múltiple.

• Herencia simple.

• Instancia.

• Ligadura.

• Mensaje.

• Método.

• Objeto.

• Objeto compuesto.

• Operaciones.

• Polimorfismo.

• Reutilización.

• Reusabilidad.

• Sobrecarga.

• TDA  TAD (ADT).

• Tipo Abstracto de Datos.

• Variable de instancia.

RESUMEN

El tipo abstracto de datos se implementa a través de clases.

Una clase es un conjunto de objetos que constituyen instancias de la clase, cada una de las cuáles tienen la misma estructura y comportamiento. Una clase tiene un nombre, una

colección de operaciones para manipular sus instancias y

una representación. Las operaciones que manipulan las instancias de una clase se llaman métodos. El estado o representación de una instancia se almacena en variables de instancia. Estos métodos se invocan mediante el envío de mensajes a instancias. El envío de mensajes a objetos (instancias) es similar a la llamada a procedimientos en lenguajes

de programación tradicionales.

El mismo nombre de un método se puede sobrecargar

con diferentes implementaciones; el método Imprimir se

puede aplicar a enteros, arrays y cadenas de caracteres. La

sobrecarga de operaciones permite a los programas ser extendidos de un modo elegante y permite la ligadura de un

mensaje a la implementación de código del mensaje y se

hace en tiempo de ejecución. Esta característica se llama

ligadura dinámica. El polimorfismo permite desarrollar sistemas en los que objetos diferentes pueden responder de

modo diferente al mismo mensaje. La ligadura dinámica,

sobrecarga y la herencia permite soportar el polimorfismo

en lenguajes de programación orientados a objetos.

• La programación orientada a objetos incorpora estos

seis componentes importantes:

— Objetos.

— Clases.

— Métodos.

— Mensajes.

— Herencia.

— Polimorfismo.

• Un objeto se compone de datos y funciones que operan sobre esos objetos.

• La técnica de situar datos dentro de objetos de modo

que no se puede acceder directamente a los datos se

llama ocultación de la información.

• Una clase es una descripción de un conjunto de objetos. Una instancia es una variable de tipo objeto y un

objeto es una instancia de una clase.

• La herencia es la propiedad que permite a un objeto

pasar sus propiedades a otro objeto, o dicho de otro

modo, un objeto puede heredar de otro objeto.

• Los objetos se comunican entre sí pasando mensajes.

• La clase padre o ascendiente se denomina clase base

y las clases descendientes, clases derivadas.

• La reutilización de software es una de las propiedades

más importantes que presenta la programación orientada a objetos.

• El polimorfismo es la propiedad por la cual un mismo

mensaje puede actuar de diferente modo cuando actúa

sobre objetos diferentes ligados por la propiedad de la

herencia.

• UML, Lenguaje Unificado de Modelado, utilizado en

el desarrollo de sistemas.

EJERCICIOS

15.1. Describa y justifique los objetos que obtiene de cada

uno de estos casos:

a) Los habitantes de Europa y sus direcciones de

correo.

b) Los clientes de un banco que tienen una caja

fuerte alquilada.

c) Las direcciones de correo electrónico de una

universidad.

P:167

Tipos abstractos de datos, objetos y modelado con UML 2.1 571

d) Los empleados de una empresa y sus claves de

acceso a sistemas de seguridad.

15.2. ¿Cuáles serían los objetos que han de considerarse

en los siguientes sistemas?

a) Un programa para maquetar una revista.

b) Un contestador telefónico.

c) Un sistema de control de ascensores.

d) Un sistema de suscripción a una revista.

15.3. Definir los siguientes términos:

a) Clase g) Miembro dato

b) Objeto h) Constructor

c) Sección de declaración

i) Instancia de una clase

d) Sección de implementación

j) Métodos o servicios

e) Variable de instancia k) Sobrecarga

f) Función miembro l) Interfaz

15.4. Deducir los objetos necesarios para diseñar un programa de computadora que permita jugar a diferentes juegos de cartas.

15.5. Determinar los atributos y operaciones que pueden

ser de interés para los siguientes objetos, partiendo

de la base que van a ser elementos de un almacén de

regalos: un libro, un disco, una grabadora de vídeo,

una cinta de vídeo, un televisor, una radio, un tostadora de pan, una cadena de música, una calculadora

y un teléfono celular (móvil).

15.6. Crear una clase que describa un rectángulo que se

pueda visualizar en la pantalla de la computadora,

cambiar de tamaño, modificar su color de fondo y

los colores de los lados.

15.7. Representar una clase ascensor (elevador) que tenga las funciones usuales de subir, bajar, parar entre

niveles (pisos), alarma, sobrecarga y en cada nivel

(piso) botones de llamada para subir o bajar.

15.8. Dibujar diagramas de objetos que representen la

jerarquía de objetos del modelo Figura.

15.9. Construir una clase Persona con las funciones

miembro y atributos que crea oportunos.

15.10. Construir una clase llamada Luz que simule una luz

de tráfico. El atributo color de la clase debe cambiar

de Verde a Amarillo y a Rojo y de nuevo regresar

a Verde mediante la función Cambio. Cuando un

objeto Luz se crea su color inicial será Rojo.

15.11. Construir una definición de clase que se pueda utilizar para representar un empleado de una compañía. Cada empleado se define por un número entero ID, un salario y el número máximo de horas de

trabajo por semana. Los servicios que debe proporcionar la clase, al menos deben permitir introducir

datos de un nuevo empleado, visualizar los datos

existentes de un nuevo empleado y capacidad para

procesar las operaciones necesarias para dar de alta

y de baja en la seguridad social y en los seguros

que tenga contratados la compañía.

P:169

CAPÍTULO 16

Diseño de clases y objetos:

Representaciones gráficas en UML

16.1. Diseño y representación gráfica de objetos

en UML

16.2. Diseño y representación gráfica de clases

en UML

16.3. Declaración de objetos de clases

16.4. Constructores

16.5. Destructores

16.6. Implementación de clases en C++

16.7. Recolección de basura

CONCEPTOS CLAVE

RESUMEN

EJERCICIOS

LECTURAS RECOMENDADAS

Hasta ahora hemos aprendido el concepto de estructuras, con lo que se ha visto un medio para agrupar

datos. También se han examinado funciones que sirven para realizar acciones determinadas a las que se

les asigna un nombre. En este capítulo se tratarán las

clases, un nuevo tipo de dato cuyas variables serán

objetos. Una clase es un tipo de dato que contiene

código (funciones) y datos. Una clase permite encapsular todo el código y los datos necesarios para gestionar un tipo específico de un elemento de programa, tal como una ventana en la pantalla, un

dispositivo conectado a una computadora, una figura

de un programa de dibujo o una tarea realizada por

una computadora. En el capítulo se aprenderá a crear

(definir y especificar) y utilizar clases individuales y en

el Capítulo 17 se verá cómo definir y utilizar jerarquías

y otras relaciones entre clases.

Los diagramas de clases son uno de los tipos de

diagramas más fundamentales en UML. Se utilizan

para capturar las relaciones estáticas de su software;

en otras palabras, cómo poner o relacionar cosas juntas. Cuando se escribe software se está constantemente tomando decisiones de diseño: qué clases hacen referencia a otras clases, qué clases "poseen"

alguna otra clase, etc. Los diagramas de clase proporcionan un medio para capturar la estructura física de

un sistema.

El paradigma orientado a objetos nació en 1969

de la mano del doctor noruego Kristin Nygaard que

intentando escribir un programa de computadora

que describiera el movimiento de los barcos a través

de un fiordo, descubrió que era muy difícil simular

las mareas, los movimientos de los barcos y las formas de la línea de la costa con los métodos de programación existentes en ese momento. Descubrió

que los elementos del entorno que trataba de modelar —barcos, mareas y línea de la costa de los fiordos— y las acciones que cada elemento podía ejecutar, constituían unas relaciones que eran más fáciles

de manejar.

Las tecnologías orientadas a objetos han evolucionado mucho pero mantiene la razón de ser del paradigma: combinación de la descripción de los elementos en un entorno de proceso de datos con las

acciones ejecutadas por esos elementos. Las clases y

los objetos como instancias o ejemplares de ellas, son

los elementos clave sobre los que se articula la orientación a objetos.

INTRODUCCIÓN

P:170

574 Fundamentos de programación

16.1. DISEÑO Y REPRESENTACIÓN GRÁFICA DE OBJETOS EN UML

Un objeto es la instancia de una clase. El señor Mackoy es un objeto de la clase Persona. Un objeto es simplemente una colección de información relacionada y funcionalidad. Un objeto puede ser algo que tenga una manifestación o correspondencia en el mundo real (tal como un objeto empleado), algo que tenga algún significado virtual

(tal como una ventana en la pantalla) o alguna abstracción adecuada dentro de un programa (una lista de trabajos a

realizar, por ejemplo). Un objeto es una entidad atómica formada por la unión del estado y del comportamiento. Proporciona una relación de encapsulamiento que asegura una fuerte cohesión interna y un débil acoplamiento con el

exterior. Un objeto revela su rol verdadero y la responsabilidad cuando envíando mensajes se convierte en parte de

un escenario de comunicaciones. Un objeto contiene su propio estado interno y un comportamiento accesible a otros

objetos.

Comportamiento visible

Estado interno

oculto

Figura 16.1. Objeto: estado y comportamiento.

El mundo en que vivimos se compone de objetos tangibles de todo tipo. El tamaño de estos objetos es variable,

pequeños como un grano de arena a grandes como una montaña o un buque de recreo. Nuestra idea intuitiva de

objeto viene directamente relacionada con el concepto de masa, es decir, la propiedad que caracteriza la cantidad

de materia dentro de un determinado cuerpo. Sin embargo, es posible definir otros objetos que no tengan ninguna

masa, tal como una cuenta corriente, una póliza de seguros, una ecuación matemática o los datos personales de un

alumno de una universidad. Estos objetos corresponden a conceptos, en lugar de a entidades físicas.

Se puede ir más lejos y extender la idea de objeto haciendo que pertenezcan a “mundos virtuales” (asociados con

la red Internet, por ejemplo) con el objeto de crear comunidades de personas que no estén localizadas en la misma

área geográfica. Objetos de software definen una representación abstracta de las entidades del mundo real con el

objeto de controlarlo o simularlo. Los objetos software pueden ir desde listas enlazadas, árboles o grafos hasta archivos completos o interfaces gráficas de usuario.

En síntesis, un objeto se compone de datos que describen el objeto y las operaciones que se pueden ejecutar sobre ese objeto. La información almacenada en un objeto empleado, por ejemplo, puede ser información de identificación (nombre, dirección, edad, titulación), información laboral (título del trabajo, salario, antigüedad), etc. Las

operaciones realizadas pueden incluir la creación del sueldo del empleado o la promoción de un empleado.

Al igual que los objetos del mundo real que nacen, viven y mueren, los objetos del mundo del software tienen

una representación similar conocida como su ciclo de vida.

Nota

Un objeto es algo que encapsula información y comportamiento. Es un término que representa una cosa concreta o del mundo real.

EJEMPLO DE OBJETOS

• Vuelo 6520 de Iberia (Santo Domingo-Madrid con escala en San Juan de Puerto Rico).

• Casa n.º 31 de la Avenida de Andalucía, en Carchelejo (Jaén).

• Flor Roja en el balcón de la terraza del profesor Mackoy.

P:171

Diseño de clases y objetos: Representaciones gráfi cas en UML 575

En el mundo real, las personas identifican los objetos como cosas que pueden ser percibidas por los cinco sentidos. Los objetos tienen propiedades específicas, tales como posición, tamaño, color, forma, textura, etc., que definen

su estado. Los objetos también tienen ciertos comportamientos que los hacen diferentes de otros objetos.

Booch1

define un objeto como “algo que tiene un estado, un comportamiento y una identidad”. Supongamos una

máquina de una fábrica. El estado de la máquina puede estar funcionando/parando (“on/of”), su potencia, velocidad

máxima, velocidad actual, temperatura, etc. Su comportamiento puede incluir acciones para arrancar y parar la máquina, obtener su temperatura, activar o desactivar otras máquinas, condiciones de señal de error o cambiar la velocidad. Su identidad se basa en el hecho de que cada instancia de una máquina es única, tal vez identificada por un

número de serie. Las características que se eligen para enfatizar en el estado y el comportamiento se apoyarán en

cómo un objeto máquina se utilizará en una aplicación. En un diseño de un programa orientado a objetos se crea una

abstracción (un modelo simplificado) de la máquina basado en las propiedades y comportamiento que son útiles en

el tiempo.

[Martin/Odell] definen un objeto como “cualquier cosa, real o abstracta, en la que se almacenan datos y aquellos

métodos (operaciones) que manipulan los datos”. Para realizar esa actividad se añaden a cada objeto de la clase los

propios datos y asociados con sus propias funciones miembro que pertenecen a la clase.

Cualquier programa orientado a objetos puede manejar muchos objetos. Por ejemplo, un programa que maneja

el inventario de un almacén de ventas al por menor utiliza un objeto de cada producto manipulado en el almacén. El

programa manipula los mismos datos de cada objeto, incluyendo el número de producto, descripción del producto,

precio, número de artículos del stock y el momento de nuevos pedidos.

Cada objeto conoce también cómo ejecutar acciones con sus propios datos. El objeto producto del programa

de inventario, por ejemplo, conoce cómo crearse a sí mismo y establecer los valores iniciales de todos sus datos,

cómo modificar sus datos y cómo evaluar si hay artículos suficientes en el stock para cumplir una petición de compra. En esencia, la cosa más importante de un objeto es reconocer que consta de datos, y las acciones que pueden

ejecutar.

Un objeto de un programa de computadora no es algo que se pueda tocar. Cuando un programa se ejecuta, la

mayoría existen en memoria principal. Los objetos se crean por un programa para su uso mientras el programa se

está ejecutando. A menos que se guarden los datos de un objeto en un disco, el objeto se pierde cuando el programa

termina (este objeto se llama transitorio para diferenciarlo del objeto permanente que se mantiene después de la terminación del programa).

Un mensaje es una instrucción que se envía a un objeto y que cuando se recibe ejecuta sus acciones. Un mensaje incluye un identificador que contiene la acción que ha de ejecutar el objeto junto con los datos que necesita

el objeto para realizar su trabajo. Los mensajes, por consiguiente, forman una ventana del objeto al mundo exterior.

El usuario de un objeto se comunica con el objeto mediante su interfaz, un conjunto de operaciones definidas por

la clase del objeto de modo que sean todas visibles al programa. Una interfaz se puede considerar como una vista

simplificada de un objeto. Por ejemplo, un dispositivo electrónico tal como una máquina de fax tiene una interfaz de

usuario bien definida; por ejemplo, esa interfaz incluye el mecanismo de avance del papel, botones de marcado, receptor y el botón “enviar”. El usuario no tiene que conocer cómo está construida la máquina internamente, el protocolo de comunicaciones u otros detalles. De hecho, la apertura de la máquina durante el período de garantía puede

anularla.

16.1.1. Representación gráfica en UML

Un objeto es una instancia de una clase. Por ejemplo, se puede tener varias instancias de una clase llamada Carro

(Coche). Un carro (coche) rojo de dos puertas, un carro azul de cuatro puertas y un carro todo terreno verde de

cinco puertas. Cada instancia de Carro es un objeto al que se puede dar un nombre o dejarlo anónimo y se representan en los diagramas de objetos. Normalmente se muestra el nombre del objeto seguido por el símbolo dos puntos

y el nombre de la clase o su tipo. Tanto el nombre del objeto como el nombre de la clase se subrayan.

En UML, un objeto se representa por un rectángulo en cuyo interior se escribe el nombre del objeto subrayado.

El diagrama de representación tiene tres modelos (Figura 16.2).

El diagrama de la Figura 16.3 representa diferentes clientes de un banco y las cuentas asociadas con cada uno de

estos clientes. Las líneas que conectan estos objetos representan los enlaces que existen entre un cliente determinado

y sus cuentas. El diagrama muestra también un rectángulo con un doblete en la esquina superior derecha; este diagra1

Booch, Grady: Análisis y diseño orientado a objetos con aplicaciones, Madrid, Díaz de Santos/Addison-Wesley, 1995.

P:172

576 Fundamentos de programación

ma representa un comentario (una nota, un texto de información libre concebida con propósito de clarificación de la

figura y de facilitar la comprensión del diagrama); las líneas punteadas implementan la conexión de cualquier elemento del modelo a una nota descriptiva o de comentario.

A veces es difícil encontrar un nombre para cada objeto, por esta razón se suele utilizar con mucha mayor frecuencia un nombre genérico en lugar de un nombre individual. Esta característica permite nombrar los objetos con

términos genéricos y evitar abreviaturas de nombres o letras, tal como se hacía antiguamente, a, b o c.

El siguiente diagrama (Figura 16.4) muestra estudiantes y profesores. La ausencia de cualquier texto precedente

delante de los dos puntos significa que estamos hablando de tipos de objetos genéricos o anónimos de tipos Estudiante y Profesor.

: Estudiante Profesor Estudiante

Figura 16.4. Diferentes representaciones de objetos (Ejemplo: Profesor y Estudiante).

16.1.2. Características de los objetos

Todos los objetos tienen tres características o propiedades fundamentales que sirven para definir a un objeto de modo

inequívoco: un estado, un comportamiento y una identidad.

BMW : Carro

Seat: Carro

milavadora: Lavadora

marca= "Fagor"

modelo= "XJ201"

numeroserie= "R2D2"

capacidad= 40

entrarRopa()

sacarRopa()

ponerEnMarcha()

Figura 16.2. Diferentes objetos: Seat y BMW de la clase Carro, milavadora de la clase Lavadora.

Mackoy

Cuenta fondos

Cuenta corriente

Cuenta ahorros

Figura 16.3. Enlaces entre objetos de las clases Cliente y Cuenta.

P:173

Diseño de clases y objetos: Representaciones gráfi cas en UML 577

Objeto = Estado + Comportamiento + Identidad

Un objeto debe tener todas o alguna de las propiedades anteriores. Un objeto sin estado o sin comportamiento

puede existir, pero un objeto siempre tiene una identidad.

Booch define un objeto a partir de su experiencia y la de sus colegas como:

Un objeto es una entidad que tiene estado, comportamiento e identidad. La estructura y comportamiento de

objetos similares se definen en sus clases comunes. Los términos instancia y objeto son intercambiables

[Booch et. al., 2007].

16.1.3. Estado

El estado agrupa los valores de todos los atributos de un objeto en un momento dado, en donde un atributo es una

pieza de información que califica el objeto contenedor. Cada atributo puede tomar un valor dado en un dominio de

definición dado. El estado de un objeto, en un momento dado, se corresponde con una selección determinada de

valores a partir de valores posibles de los diversos atributos. En esencia, un atributo es una propiedad o característica de una clase y describe un rango de valores que la propiedad podrá contener en los objetos de la clase. Una

clase podrá contener ninguno o varios atributos.

Un auto

Azul marino

1.800 centímetros cúbicos

Audi A3

150 CV

Figura 16.5. Un objeto con sus atributos.

El estado de un objeto abarca todas las propiedades (normalmente estáticas) del objeto más los valores actuales (normalmente dinámicos) de cada una de estas propiedades. [Booch et al., 2007].

Los atributos de una clase son las partes de información que representan el estado de un objeto y constituyen las

propiedades de una clase. Así los detalles de la clase Carro son atributos: color, número de puertas, potencia, etc.

Los atributos pueden ser tipos primitivos simples (enteros, reales,...), compuestos (cadena, complejo,...) o relaciones

a otros objetos complejos.

Una clase puede tener cero o más atributos. Un nombre de un atributo puede ser cualquier conjunto de caracteres,

pero dos atributos de la misma clase no pueden tener el mismo nombre. Un atributo se puede mostrar utilizando dos

notaciones diferentes: en línea o en relaciones entre clases. Además, la notación está disponible para representar otras

propiedades, tales como multiplicidad, unicidad u ordenación.

Por convenio, el nombre de un atributo puede ser de una palabra o varias palabras unidas. Si el nombre es de una

palabra se escribe en minúsculas y si es más de una palabra, las palabras se unen y cada palabra, excepto la primera,

comienzan con una letra mayúscula. La lista de atributos se sitúa en el compartimento o banda debajo del compartimento que contiene al nombre de la clase.

Nombre de Objetos

miLavadora : Lavadora instancia con nombre

:Lavadora instancia sin nombre (anónima)

P:174

578 Fundamentos de programación

UML proporciona la opción de indicar información adicional para los atributos. En la notación de la clase, se

pueden especificar un tipo para cada valor del atributo. Se pueden incluir tipos tales como cadena (string), número

de coma flotante, entero o boolean (y otros tipos enumerados). Para indicar un tipo, se utilizan dos puntos (:) para

separar el nombre del atributo del tipo. Se puede indicar también un valor por defecto para un atributo.

nombre : tipo = valor_por_defecto

miLavadora : Lavadora

marca : String = "Braun"

nombreModelo : String "OroXC"

númeroSerie : String = "GL235F"

capacidad : integer = 30

Figura 16.6. Atributos con valores por defecto.

Un atributo se representa con una sola palabra en minúsculas; por otro lado, si el nombre contiene más de una

palabra, cada palabra será unida a la anterior y comenzará con una letra mayúscula, a excepción de la primera palabra, que comenzará en minúscula. La lista de atributos se inicia en la segunda banda del icono de la clase. Todo

objeto de la clase tiene un valor específico en cada atributo. El nombre de un objeto se inicia con una letra minúscula y está precedido de dos puntos que a su vez están precedidos del nombre de la clase, y todo el nombre está

subrayado.

El nombre miComputadora:Computadora es una instancia con nombre (un objeto), pero también es posible

tener un objeto o instancia anónima y se representa tal como :Computadora.

EJEMPLOS DE OBJETOS

• El profesor Mariano.

• La ciudad de Toledo.

• La casa en Calle Real 25, Carchelejo (Jaén).

• El coche (carro) amarillo que está aparcado en la calle al lado de mi ventana.

Cada objeto encapsula una información y un comportamiento. El objeto vuelo IB 6170, por ejemplo. La fecha

de salida es el 16 de agosto de 2002, la hora de salida es 22,30 de la mañana, el número de vuelo es 6170, la compañía de aviación es Iberia, la ciudad de partida es Santo Domingo y la ciudad destino es Madrid con breve escala

en San Juan de Puerto Rico. El objeto Vuelo también tiene un comportamiento. Se conocen los procedimientos de

cómo añadir un pasajero al vuelo, quitar un pasajero del vuelo o determinar cuándo el vuelo está lleno; es decir,

añadir, quitar, estáLleno. Aunque los valores de los atributos cambiarán con el tiempo (el vuelo IB 6520 tendrá

una fecha de salida, el día siguiente, 17 de agosto), los atributos por sí mismo nunca cambiarán. El vuelo 6170 siempre tendrá una fecha de salida, una hora de salida y una ciudad de salida; es decir, sus atributos son fijos.

En UML se pueden representar los tipos y valores de los atributos. Para indicar un tipo, utilice dos puntos (:) para

separar el nombre del atributo de su tipo.

Vuelo 6520: Vuelo

Fecha de salida: 18-08-2002

:Vuelo

Fecha de salida: 16-08-2002

Los valores del atributo Fecha de salida es variable

Figura 16.7. Un objeto con atributos, sus tipos así como sus valores predeterminados.

P:175

Diseño de clases y objetos: Representaciones gráfi cas en UML 579

16.1.4. Múltiples instancias de un objeto

En un diagrama de clases se pueden representar múltiples instancias de un objeto mediante iconos múltiples. Por ejemplo, si se necesita representar una lista de vuelos de Iberia para su representación en un diagrama de clases u objetos,

en lugar de mostrar cada vuelo como un objeto independiente se puede utilizar un icono con múltiples instancias para

mostrar la lista de vuelos. La notación UML para representar instancias múltiples se representa en la Figura 16.8.

Vuelo

Figura 16.8. Instancias múltiples del objeto Vuelo.

Nota

Los atributos son los trozos de información contenidos en un objeto. Los valores de los atributos pueden cambiar

durante la vida del objeto.

16.1.5. Evolución de un objeto

El estado de un objeto evoluciona con el tiempo. Por ejemplo, el objeto Auto tiene los atributos: Marca, Color,

Modelo, Capacidad del depósito o tanque de la gasolina, Potencia (en caballos de vapor). Si el auto comienza un

viaje, normalmente se llenará el depósito (el atributo Capacidad puede tomar, por ejemplo, el valor 50 “litros” o 12

galones), el color del auto, en principio no cambiará (azul cielo), la potencia tampoco cambiará (150 Caballos, HP). Es decir, hay atributos cuyo valor va variando, tal como la capacidad (ya que a medida que avance,

disminuirá la cantidad que contiene el depósito), pero habrá otros que normalmente no cambiarán, como el color y

la potencia del auto, o la marca y el modelo, inclusive el país en donde se ha construido.

El diagrama de la Figura 16.9 representa la evolución de la clase Auto con un comentario explicativo de la disminución de la gasolina del depósito debido a los kilómetros recorridos.

Un auto (Audi A3)

Capacidad 50 litros

Después de recorrer 400 km

Un auto (Audi A3)

10 litros

Capacidad

Figura 16.9. Evolución de una clase.

P:176

580 Fundamentos de programación

Los objetos de software (objetos) encapsulan una parte del conocimiento del mundo en el que ellos evolucionan.

16.1.6. Comportamiento

El comportamiento es el conjunto de capacidades y aptitudes de un objeto y describe las acciones y reacciones de

ese objeto. Cada componente del comportamiento individual de un objeto se denomina operación. Una operación es algo que la clase puede realizar o que se puede hacer a una clase. Las operaciones de un objeto se disparan (activan) como resultado de un estímulo externo representado en la forma de un mensaje enviado a otro

objeto.

Las operaciones son las características de las clases que especifican el modo de invocar un comportamiento específico. Una operación de una clase describe qué hace una clase pero no necesariamente cómo lo hace. Por ejemplo,

una clase puede ofrecer una operación para dibujar un rectángulo en la pantalla, o bien contar el número de elementos seleccionados de una lista. UML hace una diferencia clara entre la especificación de cómo invocar un comportamiento (una operación) y la implementación real de ese comportamiento (método o función).

Las operaciones en UML se especifican en un diagrama de clase con una estructura compuesta por nombre, un

par de paréntesis (vacíos o con la lista de parámetros que necesita la operación) y un tipo de retorno.

Sintaxis operaciones

1. nombre (parámetros) : tipo_retorno

2. nombre()

CuentaCorriente

nombre :string

tipo :string

número :float

ingresar(): float

retirar(): float

Figura 16.10. Clase CuentaCorriente.

Al igual que sucede con los nombres de los atributos, el nombre de una operación se pone en minúsculas si es

una palabra; en el caso de que el nombre conste de más de una palabra se unen ambas y comienzan todas las palabras

reservadas después de la primera con una letra mayúscula. Así en la clase Lavadora, por ejemplo, puede tener las

siguientes operaciones:

aceptarRopa (r: String)

aceptarDetergente (d: String)

darBotónPrender: Boolean

darBotónApagar: Boolean

Comportamiento es el modo en que un objeto actúa y reacciona , en términos de sus cambios de estado y paso

de mensajes. [Booch et. al., 2007].

En la Figura 16.11, se disparan una serie de interacciones dependiendo del contenido del mensaje.

P:177

Diseño de clases y objetos: Representaciones gráfi cas en UML 581

Las interacciones entre objetos se representan utilizando diagramas en que los objetos que interactúan se unen a

los restantes vía líneas continuas denominadas enlaces. La existencia de un enlace indica que un objeto conoce o ve

a otro objeto. Los mensajes navegan junto a los enlaces, normalmente en ambas direcciones.

Objeto A

Objeto B

Mensaje

Objeto C

Figura 16.11. Mensaje entre objetos.

Ejemplo: El objeto A envía un mensaje Almorzar al objeto B y el objeto B envía un mensaje EcharLaSiesta

al objeto C. Las operaciones que se realizan mediante la comunicación de mensajes presuponen que el objeto B tiene

la capacidad de almorzar y que el objeto C es capaz de irse a echar la siesta (Figura 16.12).

Obj A Obj B Obj C

Comer Dormir

Figura 16.12. Envío de mensajes.

El estado y el comportamiento están enlazados; realmente, el comportamiento en un momento dado depende del

estado actual y el estado puede ser modificado por el comportamiento. Sólo es posible aterrizar un avión si está

volando, de modo que el comportamiento aterrizar sólo es válido si la información enVuelo es verdadero. Después de aterrizar la información enVuelo se vuelve falsa y la operación aterrizar ya no tiene sentido; en este

caso tendría sentido despegar, ya que la información del atributo enVuelo es falsa, cuando el avión está en tierra

pendiente de despegar. El diagrama de colaboración de clases ilustra la conexión entre el estado y el comportamiento de los objetos de las clases. En el caso del objeto Vuelo 6520, las operaciones del objeto vuelo pueden ser

añadir o quitar un pasajero y chequear para verificar si el vuelo está lleno.

:Vuelo 6520

despegar()

aterrizar()

Figura 16.13. Objeto Vuelo 6520.

Definición

El comportamiento de un objeto es el conjunto de sus operaciones.

Regla

De igual modo que el nombre de un atributo, el nombre de una operación se escribe en minúscula si consta de

una sola palabra. En caso de constar de más de una palabra, se unen y se inician todas con mayúsculas, excepto la primera. La lista de operaciones se inicia en la tercera banda del icono de la clase y justo debajo de la

línea que separa las operaciones de los atributos.

P:178

582 Fundamentos de programación

:Vuelo 6520

modelo Avión

número Pasajeros

tripulación

despejar()

aterrizar()

Figura 16.14. Representación gráfica de una clase con estado y comportamiento.

16.1.7. Identidad

La identidad es la propiedad que diferencia un objeto de otro objeto similar. En esencia, la identidad de un objeto

caracteriza su propia existencia. La identidad hace posible distinguir cualquier objeto sin ambigüedad, e independientemente de su estado. Esto permite, entre otras cosas, la diferenciación de dos objetos que tengan los atributos idénticos.

La identidad no se representa específicamente en la fase de modelado de un problema. Cada objeto tiene implícitamente una identidad. Durante la fase de implementación, la identidad se crea normalmente utilizando un

identificador que viene naturalmente del dominio del problema. Nuestros autos tienen un número de placa, nuestros teléfonos celulares tienen un número a donde podemos ser llamados y nosotros mismos podemos ser identificados por el número del pasaporte o el número de la seguridad social. El tipo de identificador, denominado

también “clave natural”, se puede añadir a los estados del objeto a fin de diferenciarlos. Sin embargo, sólo es un

artefacto de implementación, de modo que el concepto de identidad permanece independiente del concepto de

estado.

16.1.8. Los mensajes

El mensaje es el fundamento de una relación de comunicación que enlaza dinámicamente los objetos que fueron

separados en el proceso de descomposición de un módulo. En la práctica, un mensaje es una comunicación entre

objetos en los que un objeto (el cliente) solicita al otro objeto (el proveedor o servidor) hacer o ejecutar alguna acción.

Formulario 1 Mensaje Formulario 2

Figura 16.15. Comunicación entre objetos.

También se puede mostrar en UML mediante un diagrama de secuencia (Figura 16.16).

Objeto 1 Objeto 2

1. Visualizar

Figura 16.16. Diagrama de secuencia.

P:179

Diseño de clases y objetos: Representaciones gráfi cas en UML 583

El mensaje puede ser reflexivo: un objeto se envía un mensaje a sí mismo:

Objeto 1

Objeto 1 Mensaje 1

Mensaje 1

Figura 16.17. Mensaje reflexivo.

La noción de un mensaje es un concepto abstracto que se puede implementar de varias formas, tales como una

llamada a una función, un evento o suceso directo, una interrupción, una búsqueda dinámica, etc. En realidad un

mensaje combina flujos de control y flujos de datos en una única entidad. Las flechas simples indican el flujo de

control y las flechas con un pequeño círculo en el origen son flujos de datos.

Objeto 1 Objeto 2

Mensaje

Datos #

Datos @

Figura 16.18. Representación gráfica de un mensaje con flujos de control y de datos.

Tipos de mensajes

Existen diferentes categorías de mensajes:

• Constructores (crean objetos).

• Destructores (destruyen objetos).

• Selectores (devuelven todo o parte del estado de un objeto).

• Modificadores (cambian todo o parte del estado de un objeto).

• Iteradores (visitan el estado de un objeto o el contenido de una estructura de datos que incluyen varios objetos).

EJEMPLO

Esquema de una clase con métodos o funciones correspondientes a los tipos de mensajes.

clase VueloAvión

publico

// constructores

...

// destructores

...

// selectores

...

// modificadores

...

P:180

584 Fundamentos de programación

// iteradores

...

privado

// atributos del vuelo

fin_clase

16.1.9. Responsabilidad y restricciones

El icono de la clase permite especificar otro tipo de información sobre la misma: responsabilidad. La responsabilidad

es un contrato o una obligación de una clase; es una descripción de lo que ha de hacer la clase. Al crear una clase se

está expresando que todos los objetos de esa clase tienen el mismo tipo de estado y el mismo tipo de comportamiento. A un nivel más abstracto, estos atributos y operaciones son simplemente las características por medio de las cuales se llevan a cabo las responsabilidades de la clase [Booch06]. Así una clase Lavadora tiene la responsabilidad

de: “recibir ropa sucia como entrada y producir ropa limpia como salida”. Una clase Pared de una casa es responsable de conocer la altura, anchura, grosor y color de la pared; una clase SensorDeTemperatura es responsable de

medir la temperatura del carro (coche) y disparar la alarma (el piloto rojo de peligro) si esta temperatura alcanza un

valor determinado de riesgo.

Las responsabilidades de la clase se pueden escribir en la zona inferior, debajo del área que contiene la lista

de operaciones. En el icono de la clase se debe incluir la suficiente información para describir una clase de un

modo no ambiguo. La descripción de las responsabilidades de la clase es un modo informal de eliminar su ambigüedad.

Una restricción es un modo más formal de eliminar la ambigüedad. La regla que describe la restricción es encerrar entre llaves el texto con la restricción especificada y situarla cerca del icono de la clase. Por ejemplo, se puede

escribir {temperatura = 35 o 39 o 42} o bien {capacidad = 20 o 30 o 40 kg}.

UML permite definir restricciones para hacer las definiciones más explicitas. El método que emplea es un lenguaje completo denominado OCL (Object Constraint Language), Lenguaje de restricción de objetos, que tiene sus

propias reglas, términos y operadores (en el sitio oficial de OMG puede ver la documentación completa de la última

versión de OCL).

16.2. DISEÑO Y REPRESENTACIÓN GRÁFICA DE CLASES EN UML

En términos prácticos, una clase es un tipo definido por el usuario. Las clases son los bloques de construcción fundamentales de los programas orientados a objetos. Booch denomina a una clase como “un conjunto de objetos que

comparten una estructura y comportamiento comunes”.

Una clase contiene la especificación de los datos que describen un objeto junto con la descripción de las acciones

que un objeto conoce cómo ha de ejecutar. Estas acciones se conocen como servicios, métodos o funciones miembro.

El término función miembro se utiliza, específicamente, en C++. Una clase incluye también todos los datos necesarios

para describir los objetos creados a partir de la clase. Estos datos se conocen como atributos o variables. El término

atributo se utiliza en análisis y diseño orientado a objetos y el término variable se suele utilizar en programas orientados a objetos.

El mundo real se compone de un gran número de objetos que interactúan entre sí. Estos objetos, en numero sas

ocasiones, resultan muy complejos para poder ser entendidos en su totalidad. Por esta circunstancia se suelen

agrupar juntos elementos similares y con características comunes en función de las propiedades más sobresalientes e ignorando aquellas otras propiedades no tan relevantes. Este es el proceso de abstracción ya citado anteriormente.

Este proceso de abstracción suele comenzar con la identificación de características comunes de un conjunto de

elementos y prosigue con la descripción concisa de estas características en lo que convencionalmente se ha venido

en llamar clase.

Una clase describe el dominio de definición de un conjunto de objetos. Cada objeto pertenece a una clase. Las

características generales están contenidas dentro de la clase y las características especializadas están contenidas en

los objetos. Los objetos software se construyen a partir de las clases vía un proceso conocido como instanciación.

De este modo un objeto es una instancia (ejemplar o caso) de una clase.

Así pues, una clase define la estructura y el comportamiento (datos y código) que serán compartidos por un conjunto de objetos. Cada objeto de una clase dada contiene la estructura (el estado) y el comportamiento definido por

P:181

Diseño de clases y objetos: Representaciones gráfi cas en UML 585

la clase y los objetos, como se ha definido anteriormente, suelen conocerse por instancias de una clase. Por consiguiente, una clase es una construcción lógica; un objeto tiene realidad física.

Una clase es una entidad que encapsula información y comportamiento.

Cuando se crea una clase, se especificará el código y los datos que constituyen esa clase. De modo general, estos

elementos se llaman miembros de la clase. De modo específico, los datos definidos en la clase se denominan variables

miembro o variables de instancia. El código que opera sobre los datos se conoce como métodos miembro o simplemente métodos. En la mayoría de las clases, las variables de instancia son manipuladas o accedidas por los métodos

definidos por esa clase. Por consiguiente, son los métodos los que determinan cómo se pueden utilizar los datos de

la clase.

Las variables definidas en el interior de una clase se llaman variables de instancia debido a que cada instancia de

la clase (es decir, cada objeto de la clase) contiene su propia copia de estas variables. Por consiguiente, los datos

de un objeto son independientes y únicos de los datos de otro objeto.

Regla

• Los métodos y variables definidos en una clase se denominan miembros de la clase.

• En Java las operaciones se denominan métodos.

• En C+ las operaciones se denominan funciones.

• En C# las operaciones se denominan métodos, aunque también se admite el término función.

Dado que el propósito de una clase es encapsular complejidad, existen mecanismos para ocultar la complejidad

de la implementación dentro de la clase. Cada método o variable de una clase se puede señalar como público o privado. La interfaz pública de una clase representa todo lo que los usuarios externos de la clase necesitan conocer o

pueden conocer. Los métodos privados y los datos privados sólo pueden ser accedidos por el código que es miembro

de la clase. Por consiguiente, cualquier otro código que no es miembro de la clase no puede acceder a un método

privado o variable privada. Dado que los miembros privados de una clase sólo pueden ser accedidos por otras partes

de su programa a través de los métodos públicos de la clase, se puede asegurar que no sucederá ninguna acción no

deseada. Naturalmente, esto significa que la interfaz pública debe ser diseñada cuidadosamente para no exponer innecesariamente a la clase.

Una clase representa un conjunto de cosas o elementos que tienen un estado y un comportamiento común. Así, por

ejemplo, Volkswagen, Toyota, Honda y Mercedes son todos coches que se representan una clase denominada Coche

(o Carro). Cada tipo específico de coche (carro) es una instancia de una clase, o dicho de otro modo, un objeto. Los objetos son miembros de clases y una clase es, por consiguiente, una descripción de un número de objetos similares. Así

Juanes, Carlos Vives, Shakira y Paulina Rubio son miembros de la clase CantantePop, o de la clase Músico.

Un objeto es una instancia o ejemplar de una clase.

16.2.1. Representación gráfica de una clase

Una clase puede representar un concepto tangible y concreto, tal como un avión; puede ser abstracto, tal como un

documento o un vehículo (como opuesto a una factura que es tangible), o puede ser un concepto intangible tal como

inversiones de alto riesgo.

En UML 2.0 una clase se representa con una caja rectangular dividida en compartimentos, o secciones, o bandas.

Un compartimento es el área del rectángulo donde se escribe información. El primer compartimento contiene el

nombre de la clase, el segundo compartimento contiene los atributos y el tercero se utiliza para las operaciones. Se

puede ocultar o quitar cualquier compartimento de la clase para aumentar la legibilidad del diagrama. Cuando no

existe un compartimento no significa que esté vacío. Se pueden añadir compartimentos a una clase para mostrar in-

P:182

586 Fundamentos de programación

formación adicional, tal como excepciones o eventos, aunque no suele ser normal recurrir a incluir estas propiedades.

UML propone que el nombre de una clase:

• Comience con una letra mayúscula.

• El nombre esté centrado en el compartimento (banda) superior.

• Sea escrito en un tipo de letra (fuente) negrita.

• Sea escrito en cursiva cuando la clase sea abstracta.

Los atributos y operaciones son opcionales, aunque como se ha dicho anteriormente no significa que si no se

muestran implique que estén vacíos. La Figura 16.19 muestra unos diagramas más fáciles de comprender con las

informaciones ocultas.

Empleado

Empleado

Figura 16.19. Clases en UML.

Cada clase se representa como un rectángulo subdivido en tres compartimentos o bandas. El primer compartimiento contiene el nombre de la clase, el segundo contiene los atributos y el último contiene las operaciones. Por

defecto, los atributos están ocultos y las operaciones son visibles. Estos compartimentos se pueden omitir para simplificar los diagramas.

EJEMPLOS

La clase Auto contiene los atributos color, motor y velocidadMáxima. La clase puede agrupar las operaciones

arrancar, acelerar y frenar.

Nombre clase Auto Auto Auto Auto

Atributos ruedas

motor

puertas

potencia

modelo: string

motor: 2000

Operaciones() descripción: Cadena

acelerar()

frenar()

parar()

Figura 16.20. Cuatro formas diferentes de representar una clase.

Lavadora Auto

marca

potencia

volumen

color

motor

velocidad Máxima

lavar()

añadirDetergente()

secar ()

parar()

arrancar ()

acelerar ()

frenar ()

(a) (b)

Figura 16.21. Diagrama de clases: (a) Lavadora; (b) Auto.

P:183

Diseño de clases y objetos: Representaciones gráfi cas en UML 587

Reproductor/Grabador de vídeo

RG-Vídeo

marca

color

grabar ()

reproducir ()

retroceder ()

Figura 16.22. Clase RG de vídeo.

Números complejos

Son números complejos aquellos que contienen una parte real y una parte imaginaria. Los elementos esenciales de

un número complejo son sus coordenadas y se pueden realizar con ellos numerosas operaciones, tales como sumar,

restar, dividir multiplicar, etc.

Número comlejo

Formato 1

Número comlejo

Formato 2

parte real

parte imaginaria

modelo

argumento

suma ()

resta ()

multiplicación ()

división ()

suma ()

resta ()

multiplicación ()

división ()

Figura 16.23. Clases Número Complejo.

Aparato de TV

Un aparato de TV es un dispositivo electrónico de complejidad considerable pero que puede utilizar adultos y niños.

El aparato de TV ofrece un alto grado de abstracción merced a sus operaciones elementales.

Aparato televisión

marca

color

tamaño

encender ()

apagar ()

cambiar de canal ()

Figura 16.24. Clase Aparato de televisión.

Estructuras de datos

Representar los tipos abstractos de datos que manipulan las estructuras dinámicas de datos fundamentales: listas,

pilas y colas.

P:184

588 Fundamentos de programación

Pila Cola Lista

fondo

cima

cabeza

cola

frente

final

apilar ()

desapilar ()

esVacía ()

meter ()

sacar ()

cola Vacía ()

entrar ()

sacar ()

Figura 16.25. Clases de estructuras de datos.

16.2.2. Declaración de una clase

La declaración de una clase se divide en dos partes:

• La especificación de una clase describe el dominio de la definición y las propiedades de las instancias de esa

clase, correspondiendo a la noción de un tipo como se define en los lenguajes de programación convencional.

• La implementación de una clase describe cómo se implementa la especificación y contiene los cuerpos de las

operaciones y los datos necesarios para que las funciones actúen adecuadamente.

Los lenguajes modulares permiten la compilación independiente de la especificación y de la implementación de

modo que es posible validar primero la consistencia de las especificaciones (también llamados interfaces) y a continuación validar la implementación en una etapa posterior. En lenguajes de programación, el concepto de tipo, descripción y módulo se integran en el concepto de clase con mayor o menor extensión.

• En C++, la clase se implementa directamente por una construcción sintáctica que incorpora el concepto de tipo,

descripción y módulo. La clase se puede utilizar para obtener un módulo único añadiendo la palabra reservada

static delante de todas las operaciones.

• En Java, la clase también es la integración de los conceptos de tipo, descripción y módulo. También existe un

concepto más general de módulos (el paquete) que puede contener varias clases.

La división entre especificación e implementación juega un papel importante en el nivel de abstracción y, en

consecuencia, en el encapsulamiento. Las características más notables se describen en la especificación mientras que

los detalles se circunscriben a la implementación.

Especificación de una clase

Antes de que un programa pueda crear objetos de cualquier clase, la clase debe ser definida. La definición de una

clase significa que se debe dar a la misma un nombre, darle nombre a los elementos que almacenan sus datos y describir las funciones que realizarán las acciones consideradas en los objetos.

Las definiciones o especificaciones no son código de programa ejecutable. Se utilizan para asignar almacenamiento a los valores de los atributos usados por el programa y reconocer las funciones que utilizará el programa. Normalmente se sitúan en archivos diferentes de los archivos de código ejecutables, utilizando un archivo para cada clase.

Se conocen como archivos de cabecera que se almacenan con un nombre de archivo con extensión .h en el caso del

lenguaje de programación C++.

Formato

clase NombreClase

lista_de_miembros

fin_clase

NombreClase Nombre definido por el usuario que identifica a la clase (puede incluir letras,

números y subrayados como cualquier identificador válido).

lista_de_miembros Funciones y datos miembros de la clase obligatorio al final de la defi nición.

P:185

Diseño de clases y objetos: Representaciones gráfi cas en UML 589

EJEMPLO 16.1

Definición en pseudocódigo de una clase llamada Punto que contiene las coordenadas x e y de un punto en un

plano.

clase Punto

//por omisión los atributos también son privados

var

privado entero: x, y //coordenadas

//por omisión los métodos también son públicos

público entero función devolverX()

//devuelve el valor de x

inicio

devolver(x)

fin función

público procedimiento fijarX(E entero: cx)

//establece el valor de x

inicio

x ← cx

fin procedimiento

público entero función devolvery()

//devuelve el valor de x

inicio

devolver(y)

fin_función

público procedimiento fijarY(E entero: cy)

//establece el valor de x

inicio

y ← cy

fin procedimiento

fin_clase

La definición de una clase no reserva espacio en memoria. El almacenamiento se asigna cuando se crea un objeto

de una clase (instancia de una clase). Las palabras reservadas público y privado se llaman especificadores de acceso.

EJEMPLO 16.2

La definición en Java de la clase Punto es

class Punto

{

private int x, y;

public int devolverX()

{

return (8x);

}

public void fijarX(int cx)

{

x = cx;

}

P:186

590 Fundamentos de programación

public int devolverY()

{

return (y);

}

public void fijarY(int cy)

{

y = cy;

}

}

// programa que crea un objeto Punto

public class PruebaPunto

{

public static void main(String[] args)

{

Punto p;

p = new Punto();

p.fijarX(4);

p.fijarY(6);

}

}

16.2.3. Reglas de visibilidad

Las reglas de visibilidad complementan o refinan el concepto de encapsulamiento. Los diferentes niveles de visibilidad dependen del lenguaje de programación con el que se trabaje, pero en general siguen el modelo de C++, aunque

los lenguajes de programación Java y C# siguen también estas reglas. Estos niveles de visibilidad son:

• El nivel más fuerte se denomina nivel "privado"; la sección privada de una clase es totalmente opaca y

sólo los amigos (término como se conoce en C++) pueden acceder a atributos localizados en la sección privada.

• Es posible aliviar el nivel de ocultamiento situando algunos atributos en la sección "protegida" de la clase.

Estos atributos son visibles tanto para amigos como las clases derivadas de la clase servidor. Para las restantes

clases permanecen invisibles.

• El nivel más débil se obtiene situando los atributos en la sección pública de la clase, con lo cual se hacen

visibles a todas las clases.

Una clase se puede visualizar como en la Figura 16.26.

Atributos públicos

Operaciones públicas

Atributos privados

y operaciones

Figura 16.26. Representación de atributos y operaciones con una clase.

P:187

Diseño de clases y objetos: Representaciones gráfi cas en UML 591

El nivel de visibilidad se puede especificar en la representación gráfica de las clases en UML con los símbolos o

caracteres #, + y – que corresponden con los niveles público, protegido y privado, respectivamente.

Reglas de visibilidad

+ Atributo público

# Atributo protegido

– Atributo privado

+ Operación pública ()

# Operación protegida ()

– Operación privada ()

Los atributos privados están contenidos en el interior de la clase ocultos a cualquier otra clase. Ya que

los atributos están encapsulados dentro de una clase, se necesitará definir cuáles son las clases que tienen acceso

a visualizar y cambiar los atributos. Esta característica se conoce como visibilidad de los atributos. Como ya

se ha comentado, existen tres opciones de visibilidad (aunque algunos lenguajes como Java y C# admiten una cuarta opción de visibilidad denominada “paquete” o “implementación”. El significado de cada visibilidad es el siguiente:

• Público. El atributo es visible a todas las restantes clases. Cualquier otra clase puede visualizar o modificar

el valor del atributo. La notación UML de un atributo público es un signo más (+).

• Privado. El atributo no es visible a ninguna otra clase. La notación UML de un atributo privado es un signo

menos (–).

• Protegido. La clase y cualquiera de sus descendientes tienen acceso a los atributos. La notación UML de un

atributo protegido es el carácter “libra” o “almohadilla” (#).

EJEMPLO

Representar una clase Empleado

Empleado

– EmpleadoID: entero = 0

# NSS: cadena

# salario: real

+ dirección: cadena

+ ciudad: cadena

+ provincia: cadena

+ código postal: cadena

+ contratar ()

+ despedir ()

+ promover ()

+ degradar ()

# trasladar ()

Reglas prácticas de visibilidad de atributos y operaciones

En general, se recomienda visibilidad privada o protegida para los atributos.

P:188

592 Fundamentos de programación

EJEMPLO

Representación de la clase Número complejo

Número complejo

– parte real

– parte imaginaria

+ suma ()

+ resta ()

+ multiplicación ()

+ división ()

16.2.4. Sintaxis

Una clase se declara utilizando la palabra reservada clase del lenguaje UPSAM 2.0 (class en C++, Java, C#)

clase <nombre_de_clase>

//Declaración de atributos

const

[privado | público | protegido]

<tipo_de_dato> : <nombre_atributo> = <valor>

....

var

[estático] [público | privado | protegido]

<tipo_de_dato> : <nombre_atributo> = [<valor_inicial>]

....

//Declaraciones de métodos

constructor <nombre_de_clase> ([<lista_de_parámetros_formales>])

// Declaración de variables locales

inicio

...

fin_constructor

...

[estático] [abstracto] [público | privado | protegido]

<tipo_de_retorno> función <nombre_func>

([<lista_de_parámetros_formales>])

inicio

...

devolver(<resultado>)

fin_función

...

[estático] [abstracto] [público | privado | protegido]

procedimiento <nombre_proc> ([<lista_de_parámetros_formales>])

P:189

Diseño de clases y objetos: Representaciones gráfi cas en UML 593

inicio

...

fin_procedimiento

destructor <nombre_de_clase> ()

// Declaración de variables locales

inicio

...

fin_destructor

...

fin_clase

EJEMPLO

clase Mueble

var

público real: anchura

público real: altura

público real: profundidad

fin_clase

Nota

1. En Java la declaración de la clase y la implementación de los métodos se almacenan en el mismo sitio y no

se definen por separado.

2. En C++, normalmente, la declaración de la clase y la implementación de los métodos se definen separadamente.

16.3. DECLARACIÓN DE OBJETOS DE CLASES

Una vez que una clase ha sido definida, un programa puede contener una instancia de la clase, denominada un objeto de la clase. Cuando se crea una clase se está creando un nuevo tipo de dato. Se puede utilizar este tipo para

declarar objetos de ese tipo.

Formato

nombre_clase: identificador

Ejemplo

Punto: p // Clase Punto, objeto p

En algunos lenguajes de programación se requiere un proceso en dos etapas para la declaración y asignación de

un objeto.

1. Se declara una variable del tipo clase. Esta variable no define un objeto; es simplemente una variable que

puede referir a un objeto.

2. Se debe adquirir una copia física, real del objeto y se asigna a esa variable utilizando el operador nuevo (en

inglés, new). El operador nuevo asigna dinámicamente, es decir, en tiempo de ejecución, memoria para un

P:190

594 Fundamentos de programación

objeto y devuelve una referencia al mismo. Esta referencia viene a ser una dirección de memoria del objeto

asignado por nuevo. Esta referencia se almacena entonces en la variable2

.

Sintaxis

varClase = nuevo nombreClase( )

EJEMPLO

Declarar una clase Libro y crear un objeto de esa clase.

clase Libro

var

real: anchura

real: altura

real: profundidad

constructor Libro (real:a,b,c)

inicio

anchura ← a

altura ← b

profundidad ← c

fin_constructor

fin_clase

...

Las dos etapas citadas anteriormente son:

Libro: milibro // declara una referencia al objeto

miLibro = nuevo Libro(5, 30, 20) // asigna un objeto Libro

Así, la definición de un objeto Punto es:

Punto: P

El operador de acceso a un miembro (.) selecciona un miembro individual de un objeto de la clase. Las siguientes sentencias, por ejemplo, crean un punto P, que fija su coordenada x y visualiza su coordenada x.

Punto: p

P.fijarX (100);

Escribir "coordenada x es", P.devolverX()

El operador punto se utiliza con los nombres de las funciones miembro para especificar que son miembros de un

objeto.

Ejemplo: Clase DiaSemana, contiene una función Visualizar

DiaSemana: Hoy // Hoy es un objeto

Hoy.Visualizar() // ejecuta la función Visualizar

2

En Java, todos los objetos de una clase se deben asignar dinámicamente.

P:191

Diseño de clases y objetos: Representaciones gráfi cas en UML 595

16.3.1. Acceso a miembros de la clase: encapsulamiento

Un principio fundamental en programación orientada a objetos es la ocultación de la información, que significa que determinados datos del interior de una clase no se puede acceder por funciones externas a la clase. El mecanismo princi pal

para ocultar datos es ponerlos en una clase y hacerlos privados. A los datos o funciones privados sólo se puede acceder

desde dentro de la clase. Por el contrario, los datos o funciones públicos son accesibles desde el exterior de la clase.

Datos

o funciones

Datos

o funciones

Privado

Público

No accesibles

desde el exterior

de la clase

(acceso denegado)

Accesible desde el

exterior de la clase

Figura 16.27. Secciones pública y privada de una clase.

Se utilizan tres diferentes especificadores de acceso para controlar el acceso a los miembros de la clase. Son

publico, privado y protegido. Se utiliza el formato general siguiente en definiciones de la clase.

Formato

const

[privado | público | protegido] <tipo_dato>: nombre=<valor>

var

[privado | público | protegido][estatico] <tipo_dato>: <nombre>

= <valor_inicial>

[estatico][abstracto][público | privado | protegido]

<tipo_de_retorno> función <nombre> ([<parametros>])

[estatico]âbstracto][público | privado | protegido]

procedimiento <nombre> ([<parametros>])

El especificador público define miembros públicos, que son aquellos a los que se puede acceder por cualquier

función. A los miembros que siguen al especificador privado sólo se puede acceder por funciones miembro de la

misma clase o por funciones y clases amigas3

. A los miembros que siguen al especificador protegida se puede

acceder por funciones miembro de la misma clase o de clases derivadas de la misma, así como por amigas. Los

miembros público, protegido y privado pueden aparecer en cualquier orden.

En la Tabla 16.1 cada “x” indica que el acceso está permitido al tipo del miembro de la clase listado en la columna de la izquierda.

Tabla 16.1. Visibilidad

Tipo de miembro

Miembro

de la misma clase Amiga

Miembro

de una clase derivada Función no miembro

privado x x

protegido x xx

publico x xx x

3

Las funciones y clases amigas son propias de C++.

P:192

596 Fundamentos de programación

Si se omite el especificador de acceso, el acceso a los atributos se considera privado y a los métodos, publico.

En la siguiente clase Estudiante, por ejemplo, todos los datos son privados, mientras que las funciones miembro

son públicas.

clase Estudiante

var

real: numId

cadena: nombre

entero: edad

real función leerNumId()

inicio

...

fin_función

cadena función leerNombre()

inicio

...

fin_función

entero función leerEdad()

inicio

...

fin_función

fin_clase

En C++ se establecen secciones públicas y privadas en las clases y el mismo especificador de acceso puede aparecer más de una vez en una definición de una clase, pero —en este caso— no es fácil de leer.

class Estudiante{

private:

long numId;

public:

long leerNumId();

private:

char nombre[40];

int edad;

public:

char* leerNombre();

int LeerEdad();

};

El especificador de acceso se aplica a todos los miembros que vienen después de él en la definición de la clase

(hasta que se encuentra otro especificador de acceso).

Aunque las secciones públicas y privadas pueden aparecer en cualquier orden, en C++ los programadores suelen

seguir algunas reglas en el diseño que citamos a continuación y que usted puede elegir la que considere más eficiente.

1. Poner la sección privada primero, debido a que contiene los atributos (datos).

2. Se pone la sección pública primero, debido a que las funciones miembro y los constructores son la interfaz

del usuario de la clase.

La regla 2 presenta realmente la ventaja de que los datos son algo secundario en el uso de la clase y con una

clase definida adecuadamente realmente no se suele necesitar nunca ver cómo están declarados los atributos.

En realidad, tal vez el uso más importante de los especificadores de acceso es implementar la ocultación de la

información. El principio de ocultación de la información indica que toda la interacción con un objeto se debe restringir a utilizar una interfaz bien definida que permite que los detalles de implementación de los objetos sean igno-

P:193

Diseño de clases y objetos: Representaciones gráfi cas en UML 597

rados. Por consiguiente, las funciones miembro y los miembros datos de la sección pública forman la interfaz externa del objeto, mientras que los elementos de la sección privada son los aspectos internos del objeto que no necesitan

ser accesibles para usar el objeto.

El principio de encapsulamiento significa que las estructuras de datos internas utilizadas en la implementación

de una clase no pueden ser accesibles directamente al usuario de la clase.

Nota

Los lenguajes C++, Java y C# proporcionan un especificador de acceso, protected.

16.3.2. Declaración de métodos

Las clases normalmente constan de dos cosas: variables de instancia y métodos. Existen dos formatos para la declaración de los métodos, dependiendo de que se siga el modelo C++ (función miembro) o el modelo Java / C# (método).

C++: tipo_retorno NombreClase:: nombreFuncion(listaParámetros)

{

// cuerpo de la función

}

Java: tipo_retorno NombreClase(listaParámetros)

{

// cuerpo del método

}

Los métodos que tienen un tipo de retorno distinto de void devuelve un valor a la rutina llamadora utilizando el

formato siguiente de la sentencia return:

return valor;

valor es el valor devuelto.

EJEMPLO

Clase Fecha en pseudocódigo

//Pseudocódigo

clase Fecha

var

privado entero: dia, mes, anyo

procedimiento fijarFecha(E entero: d, m, a)

inicio

dia ← d

mes ← m

anyo ← a

fin_procedimiento

P:194

598 Fundamentos de programación

procedimiento mostrarFecha ()

inicio

escribir(dia, '/', mes, '/', anyo)

fin_procedimiento

fin_clase

Notación y asignación de valores a los miembros

La sintaxis estándar para la referencia y asignación de valores a miembros de una clase se utilizan los siguientes

formatos:

Formato:

nombre-objeto.nombre-atributo

nombre-objeto.nombre-función(parámetros)

Código Java

import java.io.*;

class Fecha

{

protected int dia, mes, anyo;

public void fijarFecha(int d, int m, int a)

{

dia = d;

mes = m;

anyo = a;

}

public void mostrarFecha ()

{

System.out.println(dia+"/"+mes+"/"+anyo);

}

}

class PruebaFecha

{

public static void main (String[] args)

{

Fecha f1, f2;

f1 = new Fecha();

f2 = new Fecha();

f1.dia = 15;

f1.mes = 7;

f1.anyo = 2002;

f1. mostrarFecha();

f2.fijarFecha(20, 8, 2002);

f2.mostrarFecha();

}

}

P:195

Diseño de clases y objetos: Representaciones gráfi cas en UML 599

El resultado de la ejecución es:

15/7/2002

20/8/2002

A continuación se muestra un código en C++ donde se establecen los atributos como privados y las funciones

miembro como públicas. A diferencia de Java, el modo de acceso predeterminado en C++ es private. Otra diferencia con Java es que en C++ las funciones miembro se declaran dentro de la clase pero, aunque puede hacerse dentro, suelen definirse fuera de la clase. En la definición de una función miembro fuera de la clase el nombre

de la función ha de escribirse precedido por el nombre de la clase y por el operador binario de resolución de alcance (::)

include <iostream>

//Declaración de datos miembro y funciones miembro

class Fecha

{

int dia, mes, anyo;

public:

void fijarFecha(int, int, int);

void mostrarFecha();

};

void Fecha::fijarFecha(int d, int m, int a)

{

dia = d;

mes = m;

anyo = a;

}

void Fecha::mostrarFecha ()

{

cout << dia <<"/"<< mes <<"/" << anyo;

}

//Prueba Fecha

int main()

{

Fecha f;

f.fijarFecha(20, 8, 2002);

cout << "La fecha es "

f.mostrarFecha();

cout << endl;

return 0;

}

El código en C# sería

using System;

class Fecha

{

int dia, mes, anyo;

public void fijarFecha(int d, int m, int a)

{

dia = d;

mes = m;

P:196

600 Fundamentos de programación

anyo = a;

}

public void mostrarFecha ()

{

Console.WriteLine(dia+"/"+mes+"/"+anyo);

}

}

class PruebaFecha

{

public static void Main()

{

Fecha f;

f = new Fecha();

f.fijarFecha(20, 8, 2002);

f.mostrarFecha();

}

}

Los campos de datos en este último ejemplo también son privados, por lo que, con el código especificado, la

sentencia

Console.WriteLine(f.dia);

daría error.

EJEMPLO 16.3

Los programas anteriores estaban muy simplificados, por lo que el método fijarFecha no depuraba si la fecha establecida era o no correcta. En el siguiente programa (implementado en Turbo Pascal 7.0) el método fijarFecha

sólo asigna a los atributos los valores que recibe como parámetro si éstos constituyen una fecha válida. Si la fecha

no es válida, los atributos reciben el valor 0. Los objetos en Turbo Pascal deben implementarse en unidades, que

constituyen librerías de declaraciones que se compilan por separado del programa principal.

unit uobjeto;

interface

type

Fecha = object

function fijarFecha(d, m, a: integer): boolean;

procedure mostrarFecha;

private

dia, mes, anyo: integer;

end;

implementation

function Fecha.fijarFecha( d, m, a: integer): boolean;

var

fechavalida, esbisiesto: boolean;

begin

fechavalida := true;

esbisiesto := (a mod 4=0) and (a mod 100<>0) or(a mod 400=0);

if (m < 1) or (m > 12) then

fechavalida := false

else

P:197

Diseño de clases y objetos: Representaciones gráfi cas en UML 601

if d < 1 then

fechavalida := false

else

case m of

4,6,9,11: if d > 30 then

fechavalida := false;

2: if esbisiesto and (d > 29) then

fechavalida := false

else

if not esbisiesto and (d>28) then

fechavalida := false;

else

if d > 31 then

fechavalida := false

end;

if fechavalida then

begin

dia := d;

mes := m;

anyo := a;

fijarFecha := true;

end

else

begin

dia := 0;

mes := 0;

anyo := 0;

fijarFecha := false;

end

end;

procedure Fecha.mostrarFecha;

begin

writeln(dia,'/', mes, '/', anyo)

end;

end.

program Prueba;

uses uobjeto;

var

f: Fecha;

begin

if f.fijarFecha(20, 8, 2002) then

begin

Write('La fecha es ');

f.mostrarFecha

end

else

writeln('Error')

end.

16.3.3. Tipos de métodos

Los métodos que pueden aparecer en la definición de una clase se clasifican en función del tipo de operación que

representan. Estos métodos tienen una correspondencia con los tipos de mensajes que se pueden enviar entre los objetos de una aplicación, como por otra parte era lógico pensar.

P:198

602 Fundamentos de programación

• Constructores y destructores, son funciones miembro a las que se llama automáticamente cuando un operador

se crea o se destruye.

• Selectores, que devuelven los valores de los miembros dato.

• Modificadores o mutadores, que permiten a un programa cliente cambiar los contenidos de los miembros

dato.

• Operadores, que permiten definir operadores estándar para los objetos de las clases.

• Iteradores, que procesan colecciones de objetos, tales como arrays y listas.

16.4. CONSTRUCTORES

Un constructor es un método que tiene el mismo nombre que la clase y cuyo propósito es inicializar los miembros

datos de un nuevo objeto que se ejecuta automáticamente cuando se crea un objeto de una clase. Sintácticamente es

similar a un método. Dependiendo del número y tipos de los argumentos proporcionados, una función o método

constructor se llama automáticamente cada vez que se crea un objeto. Si no se ha escrito ninguna función constructor en la clase, el compilador proporciona un constructor por defecto. A su rol como inicializador, un constructor

puede también añadir otras tareas cuando es llamado.

Un constructor tiene el mismo nombre que la propia clase. Cuando se define un constructor no se puede especificar un valor de retorno, ni incluso nada (void); un constructor nunca devuelve un valor. Un constructor puede,

sin embargo, tomar cualquier número de parámetros (cero o más).

Reglas

1. El constructor tiene el mismo nombre que la clase.

2. Puede tener cero o más parámetros.

3. No devuelve ningún valor.

EJEMPLO 16.4

La clase Rectangulo tiene un constructor con cuatro parámetros. El código se muestra en C++.

class Rectangulo

{

private:

int izdo;

int superior;

int dcha;

int inferior;

public:

//Constructor

Rectangulo(int i, int s, int d, int inf);

//Definiciones de otras funciones miembro

};

Cuando se define un objeto, se pasan los valores de los parámetros al constructor utilizando una sintaxis similar

a una llamada normal de la función:

Rectangulo Rect(25, 25, 75, 75);

Esta definición crea una instancia del objeto Rectangulo e invoca al constructor de la clase pasándole los parámetros con valores especificados.

P:199

Diseño de clases y objetos: Representaciones gráfi cas en UML 603

Se puede también pasar los valores de los parámetros al constructor cuando se crea la instancia de una clase utilizando el operador new:

Rectangulo *Crect = new Rectangulo(25, 25, 75, 75);

El operador new invoca automáticamente al constructor del objeto que se crea (esta es una ventaja importante de

utilizar new en lugar de otros métodos de asignación de memoria tales como la función malloc).

16.4.1. Constructor por defecto

Un constructor que no tiene parámetros se llama constructor por defecto. Un constructor por defecto normalmente

inicializa los miembros dato asignándoles valores por defecto.

EJEMPLO 16.5

El constructor por defecto inicializa x e y a 0

// Clase Punto implementada en C++

class Punto

{

public:

Punto

{

x = 0;

y = 0;

}

private

int x;

int y;

}

Una vez que se ha declarado un constructor, cuando se declara un objeto Punto sus miembros dato se iniciliazan

a 0. Esta es una buena práctica de programación.

Punto P1 // P1.x = 0, P1.y = 0

Si Punto se declara dentro de una función, su constructor se llama tan pronto como la ejecución del programa

alcanza la declaración de Punto:

void FuncDemoConstructorD()

{

Punto pt; // llamada al constructor

// ...

}

Regla

C++ crea automáticamente un constructor por defecto cuando no existen otros constructores. Sin embargo, tal

constructor no inicializa los miembros dato de la clase a un valor previsible, de modo que siempre es conveniente al crear su propio constructor por defecto darle la opción de inicializar los miembros dato con valores

previsibles.

P:200

604 Fundamentos de programación

Precaución

Tenga cuidado con la escritura de la siguiente sentencia:

Punto P();

Aunque parece que se realiza una llamada al constructor por defecto, lo que se hace es declarar una función de

nombre P que no tiene parámetros y devuelve un resultado de tipo Punto.

Formato

1. Un constructor debe tener el mismo nombre que la clase a la cual pertenece.

2. No tiene ningún tipo de retorno (ni incluso void).

// Programa en Java

import java.io.*;

class Rectangulo

{

private double longitud, anchura;

// constructor

Rectangulo(double l, double a)

{

longitud = l;

anchura = a;

}

double perimetro()

{

return 2*(longitud+anchura);

}

}

class PruebaRectangulo

{

public static void main(String[] args)

{

Rectangulo r;

r = new Rectangulo(3.5, 6.5);

System.out.println(r.perimetro()) ;

}

}

//Código en Turbo Pascal 7.0

unit uobjeto2;

interface

type

Rectangulo = object

constructor Rectangulo (l, a: real);

function perimetro: real;

private

longitud, anchura: real;

end;

implementation

{ pueden omitirse los parámetros, pues ya están

especificados en la declaración}

constructor Rectangulo.Rectangulo;

P:201

Diseño de clases y objetos: Representaciones gráfi cas en UML 605

begin

longitud := l;

anchura := a

end;

function Rectangulo.perimetro;

begin

perimetro := 2*(longitud + anchura)

end;

end.

program PruebaR;

uses uobjeto2;

var

p: ^Rectangulo;

begin

{new crea un objeto en el montículo y lo inicializa al

llevar como segundo parámetro el constructor}

new (p,rectangulo(3.5, 6.5));

writeln('El perímetro es ', p^.perimetro:0:2);

end.

También es válido crear una instancia del objeto Rectangulo sin emplear new e invocar al constructor utilizando una sintaxis similar a la empleada para llamar a un procedimiento.

program PruebaR;

uses uobjeto2;

var

r: Rectangulo;

begin

r.Rectangulo(3.5, 6.5);

writeln('El perímetro es ', r.perimetro:0:2);

end.

La clase Rectangulo en C++

class Rectangulo

{

int longitud;

int anchura;

public:

Rectangulo(int l, int a);

//definiciones de otras funciones miembro

}

Cuando en una clase no se declara ningún constructor, el compilador crea un constructor por defecto. El constructor

por defecto inicializa todas las variables instancia a cero o por el contrario también se refiere a aquel constructor que

no requiere la declaración de ningún parámetro o porque a todos los parámetros se les ha dado un valor por defecto.

Nota

Un constructor es cualquier función que tiene el mismo nombre que su clase. El propósito principal de un constructor es inicializar las variables miembro de un objeto cuando éste se crea. Por consiguiente, un constructor se

llama automáticamente cuando se declara un objeto.

En general, una clase puede contener múltiples constructores pero se diferencian entre sí en la lista de parámetros.

Cada constructor se debe declarar sin ningún tipo de dato de retorno (ni incluso void).

P:202

606 Fundamentos de programación

16.5. DESTRUCTORES

La contrapartida a un constructor es un destructor Los destructores son funciones (métodos) que tienen el mismo

nombre de la clase al igual que los constructores, pero para distinguirlos sintácticamente se les precede por una tilde

(~) o por la palabra reservada destructor.

Ejemplo

~Fecha ()

Al igual que sucede con los constructores, se proporciona un constructor por defecto en el caso de que no se

incluya explícitamente en la declaración de la clase. Al contrario que los constructores, sólo puede haber un destructor por clase. Esto se debe a que los destructores no pueden tener argumentos ni devolver valores.

Los destructores se llaman automáticamente siempre que un objeto deje de existir y su objetivo es limpiar cualquier efecto no deseado que haya podido dejar el objeto.

Regla

Una función destructor se llama a la vez que un objeto sale fuera de ámbito (desaparece).

Los destructores deben tener el mismo nombre que su clase pero suelen ir precedidos de una tilde.

Sólo puede haber un destructor por clase.

Un destructor no tiene argumentos ni devuelve ningún valor. Si no se incluye ningún destructor en la clase, el

compilador proporciona un destructor por defecto.

EJEMPLO 16.5

Se declara una clase con constructor y destructor.

//C++

class Demo

{

int datos;

public:

Demo() {datos = 0;} // constructor

~Demo() {} // destructor

};

Regla

• Los destructores no tienen valor de retorno.

• Tampoco tienen argumentos.

El uso más frecuente de un destructor es liberar memoria que fue asignada por el constructor. Si un destructor no

se declara explícitamente, se crea uno vacío automáticamente. Si un objeto tiene ámbito local, su destructor se llama

cuando el control pasa fuera de su bloque de definición.

P:203

Diseño de clases y objetos: Representaciones gráfi cas en UML 607

Regla en C++

Si un objeto tiene ámbito de archivo, el destructor se llama cuando termina el programa principal (main). Si un

objeto se asignó dinámicamente (utilizando new y delete), el destructor se llama cuando se invoca el operador

delete.

En C# la memoria se libera automáticamente, a través de un recolector automático de basura (Garbage Collector),

que llama a los destructores a partir del momento en el que se sabe que un objeto ya no va a ser utilizado. El recolector de basura invoca al destructor, que es el que sabe cómo liberar el recurso, en el momento que considera oportuno. En el bloque de un destructor deben especificarse las instrucciones especiales que deben ser ejecutadas al

destruir un objeto de la clase.

EJEMPLO 16.6

Programa en el que se activa el constructor y destructor de una clase C#.

//C#

using System;

class Punto

{

int x, y;

public Punto(int cx, int cy) {

x = cx;

y = cy;

}

~Punto() {

Console.WriteLine("Se ha llamado al destructor de Punto");

}

}

class PruebaDestructores

{

public static void main()

{

Punto p = new Punto(3,4);

p = null;

//la siguiente instrucción fuerza la recolección de basura

GC.Collect();

//Hace que el hilo actual espere a que la cola de

//destructores quede vacía

GC.WaitForPendingFinalizers();

}

}

Java también tiene recolección automática de basura, siendo el método finalize el que se redefine para efectuar

operaciones especiales de limpieza.

16.6. IMPLEMENTACIÓN DE CLASES EN C++

El código fuente para la implementación de funciones miembro de una clase es código ejecutable. Se almacena, por

consiguiente, en archivos de texto con extensiones .cp o .cpp. Normalmente se sitúa la implementación de cada

clase en un archivo independiente.

P:204

608 Fundamentos de programación

Cada implementación de una función tiene la misma estructura general. Obsérvese que una función comienza con

una línea de cabecera que contiene, entre otras cosas, el nombre de la función y su cuerpo está acotado entre una

pareja de signos llave. Las clases pueden proceder de diferentes fuentes:

• Se pueden declarar e implementar sus propias clases. El código fuente siempre estará disponible.

• Se pueden utilizar clases que hayan sido escritas por otras personas o incluso que se han comprado. En este

caso se puede disponer del código fuente o estar limitado a utilizar el código objeto de la implementación.

• Se puede utilizar clases de las bibliotecas del programa que acompañan a su software de desarrollo C++. La

implementación de estas clases se proporciona normalmente como código objeto.

En cualquier forma, se debe disponer de las versiones de texto de las declaraciones de clase para que pueda utilizarlas su compilador.

16.6.1. Archivos de cabecera y de clases

Las declaraciones de clases se almacenan normalmente en sus propios archivos de código fuente, independientes

de la implementación de sus funciones miembro. Estos son los archivos de cabecera que se almacenan con una

extensión .h en el nombre del archivo.

El uso de archivos de cabeceras tiene un beneficio muy importante: “Se puede tener disponible la misma declaración de clases a muchos programas sin necesidad de duplicar la declaración”. Esta propiedad facilita la reutilización

en programas C++.

Para tener acceso a los contenidos de un archivo de cabecera, un archivo que contiene la implementación de las

funciones de la clase declaradas en el archivo de cabecera o un archivo que crea objetos de la clase declarada en el

archivo de cabecera incluye (include), o mezcla, el archivo de cabecera utilizando una directiva de compilador, que

es una instrucción al compilador que se procesa durante la compilación. Las directivas del compilador comienzan

con el signo “almohadilla” (#).

// Declaración de una clase almacenada en Demo1.h

clase Demo1

public:

Demo1();

void Ejecutar();

fin_clase

// Declaración de la clase edad almacenada en edad.h

class edad

{

private:

int edadHijo, edadPadre, edadMadre;

public:

edad();

void iniciar(int, int, int);

int obtenerHijo();

int obtenerPadre();

int obtenerMafre();

};

Figura 16.28. Listado de declaraciones de clases.

P:205

Diseño de clases y objetos: Representaciones gráfi cas en UML 609

La directiva que mezcla el contenido de un archivo de cabecera en un archivo que contiene el código fuente de

una función es:

#include nombre-archivo

Opciones de compilación

La mayoría de los compiladores soporta dos versiones ligeramente diferentes de esta directiva. La primera instruye

al compilador a que busque el archivo de cabecera en un directorio de disco que ha sido designado como el depósito de archivos de cabecera.

Ejemplo

#include <iostream>

utiliza la biblioteca de clases que soporta E/S.

La segunda versión se produce cuando el archivo de cabecera está en un directorio diferente; entonces, se pone

el nombre del camino entre dobles comillas.

Ejemplo

#include "/mi.cabecera/cliente.h"

16.6.2. Clases compuestas

Una clase compuesta es aquella clase que contiene miembros dato que son así mismo objetos de clases. Antes de que

el cuerpo de un constructor de una clase compuesta, se deben construir los miembros dato individuales en su orden

de declaración.

La clase Estudiante contiene miembros dato de tipo Expediente y Dirección:

// código en C#

class Expediente

{

//...

}

class Direccion

{

//...

}

class Estudiante

{

string id;

Expediente exp;

Direccion dir;

float notaMedia;

public Estudiante()

{

PonerId ("");

PonerNotaMedia(0.0F);

dir = new Direccion();

exp = new Expediente();

}

P:206

610 Fundamentos de programación

public void PonerId (string v)

{

id = v;

}

public void PonerNotaMedia(float v)

{

notaMedia = v;

}

public void Mostrar()

{

}

}

Aunque Estudiante contiene Expediente y Direccion, el constructor de Estudiante no tiene acceso a los

miembros privados o protegidos de Expediente o Direccion. Cuando un objeto Estudiante sale fuera de alcance, se llama a su destructor. Aunque generalmente el orden de las llamadas a destructores a clases compuestas es

exactamente el opuesto al orden de llamadas de constructores, en C++ no se tiene control sobre cuándo un destructor

va a ser ejecutado, ya que son llamados automáticamente por el recolector de basura.

16.7. RECOLECCIÓN DE BASURA

Como los objetos se asignan dinámicamente, cuando estos objetos se destruyen será necesario verificar que la memoria ocupada por ellos ha quedado liberada para usos posteriores. El procedimiento de liberación es distinto según

el tipo de lenguaje utilizado.

En C++ los objetos asignados dinámicamente se deben liberar utilizando un operador delete. Por el contrario,

Java y C# tienen un enfoque diferente. Manejan la liberación de memoria de modo automático. La técnica que utilizan se denomina recolección de basura (garbage collection). Su funcionamiento es el siguiente: cuando no existe

ninguna referencia a un objeto, se supone que ese objeto ya no se necesita, y la memoria ocupada por ese objeto

puede ser recuperada (liberada). No hay necesidad de destruir objetos explícitamente como hace C++. La recolección

de basura sólo ocurre esporádicamente durante la ejecución de su programa. No sucede simplemente porque los objetos dejen de ser utilizados.

16.7.1. El método finalize ()

En ocasiones se necesita que un objeto realice alguna acción cuando se destruye. Por ejemplo, un objeto contiene

algún recurso no-Java tal como un manejador de archivos o una fuente de caracteres de Windows, entonces puede

desear asegurarse que esos recursos se liberan antes de que se destruya el objeto. El mecanismo que utilizan algunos

lenguajes, como es el caso de C# y Java, se llama finalización. Utilizando este mecanismo se pueden definir acciones

específicas que ocurrirán cuando un objeto está a punto de ser liberado por el recolector de basura.

Para añadir un finalizador a una clase, basta con definir el método finalize() (en Java). Dentro del método

finalize() se especificarán aquellas acciones que se deben ejecutar antes de que se destruya un objeto. El recolector de basura se ejecuta periódicamente comprobando que aquellos objetos que no están siendo utilizados por

ningún estado de ejecución o indirectamente referenciados por otros objetos.

Formato

protegido destructor <nobreclase>()

inicio

fin_destructor

La palabra reservada protegido (protected) es un especificador que previene el acceso al destructor por código

definido al exterior de su clase. Si no se especifica, nada es protegido.

P:207

Diseño de clases y objetos: Representaciones gráfi cas en UML 611

RESUMEN

Una clase es un conjunto de objetos que constituyen instancias de la clase, cada una de las cuales tienen la misma

estructura y comportamiento. Una clase tiene un nombre,

una colección de operaciones para manipular sus instancias

y una representación. Las operaciones que manipulan las

instancias de una clase se llaman métodos. El estado o representación de una instancia se almacena en variables de

instancia. Estos métodos se invocan mediante el envío

de mensajes a instancias. El envío de mensajes a objetos

(instancias) es similar a la llamada a procedimientos en

lenguajes de programación tradicionales.

Los principales puntos clave tratados son:

• La programación orientada a objetos incorpora estos

seis componentes importantes:

— Objetos.

— Clases.

— Métodos.

— Mensajes.

— Herencia.

— Polimorfismo.

• Un objeto se compone de datos y funciones que operan sobre esos objetos.

• La técnica de situar datos dentro de objetos de modo

que no se puede acceder directamente a los datos se

llama ocultación de la información.

• Los programas orientados a objetos pueden incluir

objetos compuestos, que son objetos que contienen otros objetos, anidados o integrados en ellos

mismos.

• Una clase es una descripción de un conjunto de objetos. Una instancia es una variable de tipo objeto y un

objeto es una instancia de una clase.

• La herencia es la propiedad que permite a un objeto

pasar sus propiedades a otro objeto, o dicho de otro

modo, un objeto puede heredar de otro objeto.

• Los objetos se comunican entre sí pasando mensajes.

• La clase padre o ascendiente se denomina clase base

y las clases descendientes, clases derivadas.

• La reutilización de software es una de las propiedades

más importantes que presenta la programación orientada a objetos.

• El polimorfismo es la propiedad por la cual un mismo

mensaje puede actuar de diferente modo cuando actúa

sobre objetos diferentes ligados por la propiedad de la

herencia.

• Una clase es un tipo de dato definido por el usuario

que sirve para representar objetos del mundo real.

CONCEPTOS CLAVE

• Clase abstracta.

• Clase compuesta.

• Comunicación entre objetos.

• Constructor.

• Declaración de acceso.

• Destructor.

• Encapsulamiento.

• Función miembro.

• Función virtual.

• Herencia.

• Instancia

• Mensaje.

• Método.

• Miembro dato.

• Objeto.

• Ocultación de la información.

• Privada.

• Protegida.

• Pública.

• Relación es-un.

• Relación tiene-un.

Importante

En Java finalize() sólo se llama antes de la recolección de basura. Si no se llama cuando un objeto sale

fuera de ámbito, significa que no se puede conocer cuando —o incluso si— se ejecutará finalize(). En consecuencia, es importante que su programa proporcione otros medios de liberar recursos del sistema.

Nota C++/Java

C++ permite definir un destructor para una clase que se llama cuando un objeto sale fuera de ámbito (se destruye).

Java no soporta destructores. La idea aproximada del destructor en Java es el método finalize() y sus

tareas son realizadas por el subsistema de recolección de basura.

P:208

612 Fundamentos de programación

• Un objeto de una clase tiene dos componentes —un

conjunto de atributos y un conjunto de comportamientos (operaciones)—. Los atributos se llaman miembros dato y los comportamientos se llaman funciones

miembro.

clase circulo

var

público real: x_centro,

y_centro, radio

público real función superficie()

inicio

...

fin función

fin_clase

• Cuando se crea un nuevo tipo de clase, se deben realizar dos etapas fundamentales: determinar los atributos y el comportamiento de los objetos.

• Un objeto es una instancia de una clase.

circulo un_circulo

• Una declaración de una clase se divide en tres secciones: pública, privada y protegida. La sección pública

contiene declaraciones de los atributos y el comportamiento del objeto que son accesibles a los usuarios del

objeto. Los constructores se recomiendan su declaración en la sección pública. La sección privada contiene las funciones miembro y los miembros dato que

son ocultos o inaccesibles a los usuarios del objeto.

Estas funciones miembro y atributos dato son accesibles sólo por la función miembro del objeto.

• El acceso a los miembros de una clase se puede declarar como privado (private, por defecto), público

(public) o protegido (protected).

clase Circulo

var

privado real: centro_x,

centro_y, radio

público real función Superficie ()

inicio

...

devolver (...)

fin_función

público procedimiento Fijar-Centro

(E real: x, y)

inicio

...

fin_procedimiento

público procedimiento Fijar-Radio

(E real: r)

inicio

...

fin_procedimiento

público real función DevolverRadio ()

inicio

...

devolver (...)

fin_función

fin_clase

Los miembros dato centro_x, centro_y y radio son ejemplos de ocultación de datos.

• El procedimiento fundamental de especificar un objeto es

circulo :c // un objeto

y para especificar un miembro de una clase

radio = 10.0 // Miembro de la clase

El operador de acceso a miembro (el operador

punto).

c.radio = 10.0;

• Un constructor es una función miembro con el mismo nombre que su clase. Un constructor no puede

devolver un tipo pero puede ser sobrecargado.

clase Complejo

...

constructor Complejo (real: x,y)

inicio

...

fin_constructor

• Un constructor es una función miembro especial que

se invoca cuando se crea un objeto. Se utiliza normalmente para inicializar los atributos de un objeto. Los

argumentos por defecto hacen al constructor más

flexible y útil.

• El proceso de crear un objeto se llama instanciación

(creación de instancia).

• Un destructor es una función miembro especial que

se llama automáticamente siempre que se destruye un

objeto de la clase.

destructor Complejo ()

inicio

...

fin_destructor

P:209

Diseño de clases y objetos: Representaciones gráfi cas en UML 613

EJERCICIOS

16.1. Consideremos una pila como un tipo abstracto de

datos. Se trata de definir una clase que implementa

una pila de 100 caracteres mediante un array. Las

funciones miembro de la clase deben ser:

meter, sacar, pilavacia y pilallena.

16.2. Rescribir la clase pila que utilice una lista enlazada

en lugar de un array (sugerencia: utilice otra clase

para representar los modos de la lista).

16.3. Crear una clase llamada hora que tenga miembros

datos separados de tipo int para horas, minutos y

segundos. Un constructor inicializará este dato a 0 y

otro lo inicializará a valores fijos. Una función

miembro deberá visualizar la hora en formato

11:59:59. Otra función miembro sumará dos objetos

de tipo hora pasados como argumentos. Una función

principal main() crea dos objetos inicializados y

uno que no está inicializado. Sumar los dos valores

inicializados y dejar el resultado en el objeto no inicializado. Por último, visualizar el valor resultante.

16.4. Crear una clase llamada empleado que contenga

como miembro dato el nombre y el número de empleado y como funciones miembro leerdatos() y

verdatos() que lean los datos del teclado y los

visualice en pantalla, respectivamente.

Escribir un programa que utilice la clase, creando un array de tipo empleado y luego llenándolo con

datos correspondientes a 50 empleados. Una vez rellenado el array, visualizar los datos de todos los empleados.

16.5. Se desea realizar una clase Vector3d que permita

manipular vectores de tres componentes (coordenadas x, y, z) de acuerdo a las siguientes normas:

• Sólo posee una función constructor y es en línea.

• Tiene una función miembro igual que permite saber si dos vectores tienen sus componentes o coordenadas iguales (la declaración de igual se realizará utilizando: a) transmisión por valor; b)

transmisión por dirección; c) transmisión por referencia).

16.6. Incluir en la clase vector3d del ejercicio anterior

una función miembro denominada normamax que

permita obtener la norma mayor de dos vectores.

(Nota: La norma de un vector v = x, y, z es x2

+ y2

+

z2

o bien x*x + y*y + z*z).

16.7. Incluir en la clase Vector3d del ejercicio anterior

las funciones miembros suma (suma de dos vectores), productoescalar (producto escalar de dos

vectores: v1= x1, y1, z1; v2 = x2, y2, z2;

v1* v2 = x1* x2 + y1 * y2 + z1 * z2).

16.8. Realizar una clase Complejo que permita la gestión de números reales (un número complejo = dos

números reales real (doble): una parte real +

una parte imaginaria). Las operaciones a implementar son las siguientes:

• Una función establecer() permite inicializar

un objeto de tipo Complejo a partir de dos componentes double.

• Una función imprimir() realiza la visualización formateada de un Complejo.

• Dos funciones agregar() (sobrecargadas) permiten añadir, respectivamente, un Complejo a

otro y añadir dos componentes real a un Complejo.

16.9. Escribir una clase Conjunto que gestione un conjunto de enteros (entero) con ayuda de una tabla

de tamaño fino (un conjunto contiene una lista no

ordenada de elementos y se caracteriza por el hecho

de que cada elemento es único: no se debe encontrar dos veces el mismo valor en la tabla). Las operaciones a implementar son las siguientes:

• La función vacía() vacía el conjunto.

• La función agregar() añade un entero al conjunto.

• La función eliminar() retira un entero del

conjunto.

• La función copiar() recopila un conjunto en

otro.

• La función es_miembro() reenvía un valor

booleano (lógicos que indica si el conjunto contiene un elemento, un entero dado).

• La función es_igual() reenvía un valor booleano que indica si un conjunto es igual a otro.

• La función imprimir() realiza la visualización

formateada del conjunto.

16.10. Crear una clase Lista que realice las siguientes

tareas:

• Una lista simple que contenga cero o más elementos de algún tipo específico.

• Crear una lista vacía.

• Añadir elementos a la lista.

• Determinar si la lista está vacía.

• Determinar si la lista está llena.

• Acceder a cada elemento de la lista y realizar

alguna acción sobre ella.

16.11. Añadir a la clase Hora del ejercicio 16.3 las funciones de acceso, una función adelantar(int h,

int m, int s) para adelantar la hora actual de

un objeto existente, una función reiniciar(int

h, int m, int s) que reinicializa la hora actual

de un objeto existente y una función imprimir().

P:210

614 Fundamentos de programación

16.12. Añadir a la clase Complejo del ejercicio 16.8 las

operaciones:

• Suma: a + c = (A+ , (B + D)i).

• Resta: a – c = (A – C, B – D)i).

• Multiplicación: a * c = (A * C – B * D,

(A * D + B * C)i).

• Multiplicación: x * c = (x * C, x * Di),

donde x es real.

• Conjugado: ~a = (A ;-Bi).

16.13. Implementar una clase Random (aleatoria) para generar números pseudoaleatorios.

16.14. Implementar una clase Fecha con miembros dato

para el mes, día y año. Cada objeto de esta clase

representa una fecha, que almacena el día, mes y

año como enteros. Se debe incluir un constructor

por defecto, funciones de acceso, una función

reiniciar (int d, int m, int a) para

reiniciar la fecha de un objeto existente, una función adelantar(int d, int m, int a) para

avanzar a una fecha existente (día, d, mes, m, y

año, a) y una función imprimir(). Utilizar una

función de utilidad normalizar() que asegure

que los miembros dato están en el rango correcto

1 ≤ año, 1 ≤ mes ≤ 12, día ≤ días (Mes),

donde dias(Mes) es otra función que devuelve el

número de días de cada mes.

16.15. Ampliar el programa anterior de modo que pueda

aceptar años bisiestos. Nota: un año es bisiesto si

es divisible por 400, o si es divisible por 4 pero no

por 100. Por ejemplo, el año 1992 y 2000 son años

bisiestos y 1997 y 1900 no son bisiestos.

LECTURAS RECOMENDADAS

Booch, G.: Object-Oriented Analysis and Design with

Applications, Reedwood City, CA (USA): BenjaminCummings, 1994.

Rumbaugh, J.; Blaha, M.; Premerlani, W.; Eddy, F., y

Lorensen, W.: Object-Oriented Modeling and Design,

Englewood Cliffs, NJ (USA), Prentice-Hall, 1991.

Wirfs-Brock, R.; Wilkerson, B., y Wiener, L.: Designing

Object-Oriented Software, Englewood Cliffs, NJ

(USA), Prentice-Hall, 1990.

Joyanes, Luis: Programación Orientada a Objetos, 2.ª edición, Madrid, McGraw-Hill, 1998.

P:211

CAPÍTULO 17

Relaciones entre clases: Delegaciones,

asociaciones, agregaciones, herencia

17.1. Relaciones entre clases

17.2. Dependencia

17.3. Asociación

17.4. Agregación

17.5. Jerarquía de clases: generalización y especialización

17.6. Herencia: clases derivadas

17.7. Accesibilidad y visibilidad en herencia

17.8. Un caso de estudio especial: herencia múltiple

17.9. Clases abstractas

CONCEPTOS CLAVE

RESUMEN

EJERCICIOS

En este capítulo se introducen los conceptos fundamentales de relaciones entre clases. Las relaciones

más importantes soportadas por la mayoría de las metodologías de orientación a objetos y en particular por

UML son: asociación, agregación y generalización/especialización. En el capítulo se describen estas relaciones así como las notaciones gráficas correspondientes

en UML.

De modo especial se introduce el concepto de

herencia como exponente directo de la relación de generalización/especialización y se muestra cómo crear

clases derivadas. La herencia hace posible crear jerarquías de clases relacionadas y reduce la cantidad de

código redundante en componentes de clases. El

soporte de la herencia es una de las propiedades que

diferencia los lenguajes orientados a objetos de los lenguajes basados en objetos y lenguajes estructurados.

La herencia es la propiedad que permite definir

nuevas clases usando como base a clases ya existentes. La nueva clase (clase derivada) hereda los atributos y comportamiento que son específicos de ella. La

herencia es una herramienta poderosa que proporciona un marco adecuado para producir software fiable, comprensible, bajo coste, adaptable y reutilizable.

INTRODUCCIÓN

P:212

616 Fundamentos de programación

17.1. RELACIONES ENTRE CLASES

Una relación es una conexión semántica entre clases. Permite que una clase conozca sobre los atributos, operaciones

y relaciones de otras clases. Las clases no actúan aisladas entre sí, al contrario las clases están relacionadas unas con

otras. Una clase puede ser un tipo de otra clase —generalización— o bien puede contener objetos de otra clase de

varias formas posibles, dependiendo de la fortaleza de la relación entre las dos clases.

La fortaleza de una relación de clases [Miles, Hamilton 2006] se basa en el modo de dependencia de las clases

implicadas en las relaciones entre ellas. Dos clases que son fuertemente dependientes una de otra se dice que están

acopladas fuertemente y en caso contrario están acopladas débilmente.

Ventana Cursor

Figura 17.1. Relaciones entre clases.

Las relaciones entre clases se corresponden con las relaciones entre objetos físicos del mundo real, o bien objetos

imaginarios en un mundo virtual. En UML las formas en las que se conectan entre sí las clases, lógica o físicamente, se modelan como relaciones. En el modelado orientado a objetos existen tres clases de relaciones muy importantes: dependencias, generalizaciones-especializaciones y asociaciones [Booch 2006]:

• Las dependencias son relaciones de uso.

• Las asociaciones son relaciones estructurales entre objetos. Una relación de asociación “todo/parte”, en la cual

una clase representa un cosa grande (“el todo”) que consta de elementos más pequeños (“las partes”) se denomina agregación.

• Las generalizaciones conectan clases generales con otras más especializadas en lo que se conoce como relaciones subclase/superclase o hijo/padre.

Una relación es una conexión entre elementos. En el modelado orientado a objetos una relación se representa

gráficamente con una línea (continua, punteada) que une las clases.

17.2. DEPENDENCIA

La relación más débil que puede existir entre dos clases es una relación de dependencia. Una dependencia entre clases significa que una clase utiliza, o tiene conocimiento de otra clase, o dicho de otro modo “lo que una clase necesita conocer de otra clase para utilizar objetos de esa clase” (Russ Miles & Kim Hamilton, Learning UML 2.0,

O´Reilly, páginas 81-82). Normalmente es una relación transitoria y significa que una clase dependiente interactúa

brevemente con la clase destino, pero normalmente no tiene con ella una relación de un tiempo definido. Una dependencia es una relación de uso que declara que un elemento utiliza la información y los servicios de otro elemento

pero no necesariamente a la inversa.

La dependencia se lee normalmente como una relación “...usa un... “. Por ejemplo, si se tiene una clase Ventana que envía un aviso a una clase llamada EventoCerrarVentana cuando está próxima a abrirse. Entonces se dice

que Ventana utiliza EventoCerrarVentana.

Otro ejemplo puede ser una clase InterfazUsuario que depende de otra clase EntradaBlog ya que necesita

leer el contenido de la entrada de un blog (página web) para visualizar al usuario.

En un diagrama de clases, la dependencia se representa utilizando una línea discontinua dirigida hacia el elemento del cual depende. La flecha punteada de dependencia (Figura 17.2) de la página siguiente que significa “se utiliza

simplemente cuando se necesita y se olvida luego de ella”.

Ventana EventoCerrarVentana

Figura 17.2. Relación de dependencia. Ventana depende de la clase EventoCerrarVentana

porque necesitará leer el contenido de esta clase para poder cerrar la ventana

P:213

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 617

Otro ejemplo de dependencia se muestra entre la clase Interfaz y la clase EntradaBlog ya que ambas clases

trabajan juntas. Interfaz necesitará leer el contenido de las entradas del blog para visualizar estas entradas al usuario.

Las dependencias se usarán cuando se quiera indicar que un elemento utiliza a otro. Una dependencia implica

que los objetos de una clase pueden trabajar juntos; por consiguiente, se considera que es la relación directa más

débil que puede existir entre dos clases

17.3. ASOCIACIÓN

Una asociación es más fuerte que la dependencia y normalmente indica que una clase recuerda o retiene una relación

con otra clase durante un período determinado de tiempo. Es decir, las clases se conectan juntas conceptualmente en

una asociación. La asociación realmente significa que una clase contiene una referencia a un objeto u objetos, de la

otra clase en la forma de un atributo. La asociación se representa utilizando una simple línea que conecta las dos

clases, como se muestra en la Figura 17.3.

Una asociación es una relación estructural que especifica que los objetos de una clase están conectados con los

objetos de otra clase. En general, si se encuentra que una clase trabaja con un objeto de otra clase, entonces la relación entre esas clases es una buena candidata para una asociación en lugar de una dependencia.

Gráficamente, una asociación se representa como una línea continua que conecta la misma o diferentes clases.

Las asociaciones se deben utilizar cuando se desee representar relaciones estructurales. Los adornos de una asociación

son: línea continua, nombre de la asociación, dirección del nombre mediante una flecha que apunta en la dirección

de una clase a la otra. La navegabilidad se aplica a una relación de asociación que describe qué clase contiene el

atributo que soporta la relación.

Jugador Equipo

Jugador Equipo

Empleado Empleador

Juega_en

Juega_en

Figura 17.3. Relación de asociación (Jugador-Equipo).

Si se encuentra que una clase trabaja con un objeto de otra clase, entonces la relación entre clases es candidata

a una asociación en lugar de a una dependencia. Cuando una clase se asocia con otra clase, cada una juega un rol

dentro de la asociación. El rol se representa cerca de la línea próxima a la clase. En la asociación entre un Jugador

y un Equipo, si esta es profesional, el equipo es el Empleador y el jugador es el Empleado.

Una asociación puede ser bidireccional. Un Equipo emplea a jugadores.

Jugador Jugador

Juega_en

Empleado

Figura 17.4. Relación de asociación bidireccional.

También pueden existir asociaciones entre varias clases, de modo que varias clases se pueden conectar a una clase.

Defensa

Delantero

Portero

Juega_en

Juega_en

Juega_en

Equipo

Figura 17.5. Asociación entre varias clases.

P:214

618 Fundamentos de programación

Una asociación es una conexión conceptual o semántica entre clases. Cuando una asociación conecta dos clases,

cada clase envía mensajes a la otra en un diagrama de colaboración. Una asociación es una abstracción de los enlaces que existen entre instancias de objetos. Los siguientes diagramas muestran objetos enlazados a otros objetos y

sus clases correspondientes asociadas. Las asociaciones se representan de igual modo que los enlaces. La diferencia

entre un enlace y una asociación se determina de acuerdo al contexto del diagrama.

Jugador Equipo

Mackoy : Jugador Carchelejo : Equipo

Universidad Estudiante

UPSA : Universidad Mackoy : Estudiante

Una asociación

Un enlace

Una asociación

Un enlace

Juega_en

Estudia_en

Figura 17.6. Asociación entre clases.

Regla

El significado más típico es una conexión entre clases, es una relación semántica entre clases. Se dibuja con

una línea continua entre las dos clases. La asociación tiene un nombre (cerca de la línea que representa la asociación), normalmente un verbo, aunque está permitido los nombres o frases nominales. Cuando se modela un

diagrama de clases, se debe reflejar el sistema que se está construyendo y por ello los nombres de la asociación

deben deducirse del dominio del problema, al igual que sucede con los nombres de las clases.

Programador Computador utiliza

Figura 17.7. Un programador utiliza un computador.

La clase Programador tiene una asociación con la clase Computador.

Es posible utilizar asociaciones navegables añadiendo una flecha al final de la asociación. La flecha indica que

la asociación sólo se puede utilizar en la dirección de la flecha.

Persona Auto posee 0..*

Figura 17.8. Una asociación navegable representa a una persona que posee (es propietaria)

de varios carros, pero no implica que un auto pueda ser propiedad de varias personas.

Las asociaciones pueden tener dos nombres, uno en cada dirección.

Persona Auto

1..* posee 0..*

es propiedad

Figura 17.9. Una asociación navegable en ambos sentidos,

con un nombre en cada dirección.

P:215

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 619

Las asociaciones pueden ser bidireccionales o unidireccionales. En UML las asociaciones bidireccionales se dibujan con flechas en ambos sentidos. Las asociaciones unidireccionales contienen una flecha que muestra la dirección

de navegación.

En las asociaciones se pueden representar los roles o papeles que juegan cada clase dentro de las mismas. La

Figura 17.10 muestra como se representan los roles de las clases. Un nombre de rol puede ser especificado en cualquier lado de la asociación. El siguiente ejemplo ilustra la asociación entre la clase Universidad y la clase Persona. El diagrama especifica que algunas personas actúan como estudiantes y algunas otras personas actúan como

profesores. La segunda asociación también lleva un nombre de rol en la clase Universidad para indicar que la

universidad actúa como un empresario (empleador) para sus profesores. Los nombres de los roles son especialmente

interesantes cuando varias asociaciones conectan dos clases idénticas.

Jugador Equipo Juega_en

Empleado Empresa

Figura 17. 10. Roles en las asociaciones.

17.3.1. Multiplicidad

Entre asociaciones existe la propiedad de la multiplicidad: número de objetos de una clase que se relaciona con un

único objeto de una clase asociada (un equipo de futbol tiene once jugadores).

Equipo Jugador 1 11

Figura 17.11. Multiplicidad en una asociación.

En notación moderna, a esta relación se le suele llamar también “tiene un”, pero hay que tener cuidado porque

este concepto es sutil y de hecho siempre ha representado a la agregación, pero como se verá después UML contempla que la agregación posee (... owns a...). Por esta razón nos inclinaremos en considerar la relación “tiene-un” (has-a)

como la relación de agregación.

La multiplicidad representa la cantidad de objetos de una clase que se relacionan con un objeto de la clase asociada. La información de multiplicidad aparece en el diagrama de clases a continuación del rol correspondiente. La

multiplicidad se escribe como una expresión con un valor mínimo y un valor máximo, que pueden ser iguales; se

utilizan dos puntos consecutivos para separar ambos valores. Cuando se indica una multiplicidad en un extremo de

una asociación se está especificando cuántos objetos de la clase de ese extremo pueden existir por cada objeto de la

clase en el otro extremo. UML utiliza un asterisco (*) para representar más y representa muchos. La Tabla 17.1 resume los valores más típicos de multiplicidad.

Tabla 17.1. Multiplicidad en asociaciones

Símbolo Significado

1

0 .. 1

m .. n

*

0 .. *

1 .. *

2

5 .. 11

5, 10

Uno y sólo uno

Cero o uno

De m a n (enteros naturales)

De cero a muchos (cualquier entero positivo)

De cero a muchos (cualquier entero positivo)

De uno a muchos (cualquier entero positivo)

Dos

Cinco a once

Cinco o diez

Si no se especifica multiplicidad, es uno (1) por omisión. La multiplicidad se muestra cerca de los extremos de

la asociación, en la clase donde es aplicable.

P:216

620 Fundamentos de programación

EJEMPLO 17.1

Relación de asociación entre las clases Empresa y Persona.

Empresa Persona 1 1..*

Cada objeto Empresa tiene como empleados, 1 o más objetos Persona (Multiplicidad 1.. *); pero cada

objeto Persona tiene como patrón a cero o más objetos Empresa.

17.3.2. Restricciones en asociaciones

En algunas ocasiones, una asociación entre dos clases ha de seguir una regla. En este caso, la regla se indica poniendo una restricción cerca de la línea de la asociación que se representa por el nombre encerrado entre llaves.

EJEMPLO 17.2

Un cajero de un banco (humano o electrónico) atiende a clientes. La atención a los clientes se realiza en el orden

en que se colocan ante la ventanilla o mostrador, o bien en función del momento de la petición electrónica de acceso al cajero.

CajeroBanco Cliente

atiende {ordered}

Figura 17.12. Restricción en una asociación.

En algunas ocasiones las asociaciones pueden establecer una restricción entre las clases. Las restricciones típicas

pueden ser {ordenado} {or}.

17.3.3. Asociación cualificada

Cuando la multiplicidad de una asociación es de uno a muchos, se puede reducir esta multiplicidad de uno a uno con

una cualificación. El símbolo que representa la cualificación es un pequeño rectángulo adjunto a la clase correspondiente.

Banco Número de cuenta Cliente

Figura 17.13. Asociación cualificada.

17.3.4. Asociaciones reflexivas

A veces, una clase es una asociación consigo misma. Esta situación se puede presentar cuando una clase tiene objetos que pueden jugar diferentes roles.

Piloto

Persona a Bordo 1

0 .. 25

Conduce

Pasajero

Figura 17.14. Asociación reflexiva.

P:217

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 621

17.3.5. Diagrama de objetos

Los objetos se pueden representar en diagramas de objetos. Un diagrama de objetos en UML tiene la misma notación

y relaciones que un diagrama de clases, dado que los objetos son instancias de las clases. Así, un diagrama de clases

muestra los tipos de clases y sus relaciones, mientras que el diagrama de objetos muestra instancias específicas de

esas clases y enlaces específicos entre esas instancias en un momento dado. El diagrama de objetos muestra también

cómo los objetos de un diagrama de clases se pueden combinar con cada uno de los restantes en un cierto instante

de tiempo.

Profesor

nombre: String

edad: integer

Computador

nombre: String

memoria: integer

Mackoy: Profesor

nombre= "J Mackoy"

edad= 55

PC de Mackoy: Computador

nombre= "HP 50G"

memoria= 2048

utiliza

Figura 17.15. Diagrama de clases y diagrama de objetos.

Enlaces

Al igual que un objeto es una instancia de una clase, una asociación tiene también instancias. Por ejemplo, la asociación de la Figura 17.16.

Jugador Equipo juega_en

Abel:Jugador RealJaen:Equipo juega_en

Figura 17.16. Instancia de una asociación.

17.3.6. Clases de asociación

Es frecuente encontrarse con una asociación que introduce nuevas clases. Una clase se puede conectar a una asociación, en cuyo caso se denomina clase asociación. De hecho una asociación puede tener atributos y operaciones tal

como una clase, este es el caso de la clase asociación.

La clase asociación no se conecta a ninguno de los extremos de la asociación, sino que se conecta a la asociación

real, a través de una línea punteada. La clase asociación se utiliza para añadir información extra en un enlace, por

ejemplo, el momento en que fue creado. Cada enlace de la asociación se relaciona a un objeto de la clase asociación.

La clase asociación se utiliza para añadir información extra en un enlace, por ejemplo, el momento en que se crea el

enlace. Cada enlace de la asociación se relaciona a un objeto de la clase asociación.

Jugador Equipo Contratado por

Contrata

Contrato DirectorGeneral Negocia

Figura 17.17. La clase asociación Contrato está asociada con la clase DirectorGeneral.

P:218

622 Fundamentos de programación

Una clase asociación es una asociación —con métodos y atributos— que es también una clase normal. La clase

asociación se representa con una línea punteada que la conecta a la asociación que representa.

Las clases de asociación se pueden aplicar en asociaciones binarias y n-arias. De modo similar a como una clase

define las características de sus objetos, incluyendo sus características estructurales y sus características de comportamiento, una clase asociación se puede utilizar para definir las características de sus enlaces, incluyendo sus características estructurales y características de comportamiento. Estos tipos de clases se utilizan cuando se necesita mantener información sobre la propia relación.

EJEMPLO 17.3

Clase asociación EquipoFutbol.

Futbolista

– nombre: sting

– número: int

–  nombre: sting

– numEquipos: int

– fechaInicio: Date

LigaFutbol

– nombre: sting

– numJugadores: int

– ciudad: String

EquipoFutbol

* 1

Figura 17.18. Clase asociación EquipoFutbol.

Criterios de diseño

Cuando se traducen a código, las relaciones con clases asociación se obtienen, normalmente, tres clases: una por cada

extremo de la asociación y una por la propia clase asociación.

EJERCICIO 17.1

Diseñar el control de 5 ascensores (elevadores) de un edificio comercial que tenga presente las peticiones de viaje

de los diferentes clientes, en función del orden y momento de llamada.

Análisis

El control de ascensores tiene un enlace con cada uno de los 5 ascensores y otro enlace con el botón (pulsador) de

llamada de “subida/bajada”. Para gestionar el control de llamadas de modo que responda el ascensor, que cumpla con

los requisitos estipulados (situado en piso más cercano, parado, en movimiento, etc.) se requiere una clase Cola que

almacene las peticiones tanto del ControlAscensor como del propio ascensor (los motores interiores del ascensor).

Cuando el control del ascensor elige un ascensor para realizar la petición de un pasajero externo al ascensor, un pasajero situado en un determinado piso o nivel, el control del ascensor lee la cola y elige el ascensor que está situado,

disponible y más próximo en la Cola. Esta elección normalmente se realizará por algún algoritmo inteligente.

En consecuencia, se requieren cuatro clases: ControlAscensor, Ascensor (elevador), Botón (pulsador) y Cola.

La clase Cola será una clase asociación ya que puede ser requerida tanto por el control de ascensores como por

cualquier ascensor.

Recuerde el lector que una estructura de datos cola es una estructura en la que cada elemento que se introduce

en la cola es el primer elemento que sale de la cola (al igual que sucede con la cola para sacar una entrada de cine,

P:219

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 623

comprar el pan o una cola de impresoras conectadas a una computadora central). En cada enlace entre los ascensores

y el control de ascensores hay una cola. Cada cola almacena las peticiones del control del ascensor y el propio ascensor (los botones internos del ascensor).

Ascensor

Cola

ControlAscensores

Botón

*

1

1 5

Figura 17.19. Diagrama de clases para control de ascensores.

Asociaciones ternarias

Las clases se pueden asociar una a una o bien se pueden asociar unas con otras. La asociación ternaria es una asociación especial que asocia a tres clases. La asociación ternaria se representa con la figura geométrica “rombo” y con

los roles y multiplicidad necesarias, pero no están permitidos los cualificadores ni la agregación. Se puede conectar

una clase asociación a la asociación ternaria, dibujando una línea punteada a uno de los cuatro vértices del rombo.

EJERCICIO 17.2

Dibujar un modelo de seguros de automóviles que represente: compañía de seguros, asegurados, póliza de seguro y

el contrato de seguro.

Poliza

Seguro

Asegurado

Aseguradora

1..*

0..1

Compañía

Seguros

0..* 1

0..*

Cliente

Contrato

Seguro

Figura 17.20. Diagrama de clases con una asociación ternaria.

P:220

624 Fundamentos de programación

El núcleo muestra un cliente —juega el rol o papel de asegurado— que puede tener 0 o muchos contratos de

seguros y cada contrato de seguro está asociado con una única compañía de seguros que juega el rol de aseguradora.

En la asociación entre cliente y contrato de seguro, hay una o ninguna pólizas de seguros.

Asociaciones cualificadas

Una asociación cualificada se utiliza con asociaciones una-a-muchas o muchas-a-muchas para reducir su multiplicidad a una, con objeto de especificar un objeto único (o grupos de objetos) desde el destino establecido. La asociación

cualificada es muy útil para modelar cuando se busca o navega para encontrar objetos específicos en una colección

determinada.

Ejemplos típicos son los sistemas de reservas de pasaje de avión, de entradas de cine, de reservas de habitaciones

en hoteles. Cuando se solicita un pasaje, una entrada o una habitación, es frecuente que nos den un localizador de la

reserva (H234JK, o similar) que se ha de proporcionar físicamente a la hora de sacar el pasaje en el aeropuerto, la

entrada en el cine o ir a alojarse en el hotel.

El atributo o calificador se conoce normalmente como identificador (número de ID). Existen numerosos calificadores tales como: ID de reserva, nombre, número de la tarjeta de crédito, número de pasaporte, etc. En terminología

de proceso de datos, también se conoce como clave de búsqueda. Este identificador o calificador, al especificar una

clave única resuelve la relación uno a muchos y lo convierte en uno a uno.

El calificador se representa como una caja o rectángulo pequeño que se dibuja en el extremo correspondiente de

la asociación, al lado de la clase a la cual está asociada. El calificador representa un añadido a la línea de la asociación y fuera de la clase. Las asociaciones cualificadas reducen la multiplicidad real en el modelo, de uno-a-muchos

a uno-a-uno indicando con el calificador una identidad para cada asociación.

EJEMPLO 17.4

1. Lista de reservas. Calificador, ID de la reserva.

Reservas

Reservas

ListaReservas

ListaReservas

IDReserva

1 *

1 1

Figura 17.21. Asociación cualificada.

2. Lista de pasajes: vuelo Madrid-Cartagena de Indias con la compañía aérea Iberia.

Pasajeros

Localizador

Vuelo

Madrid-Cartagena

Vuelo

Madrid-Cartagena Pasajeros

*

1 1

1

Figura 17.22. Asociación cualificada.

Asociaciones reflexivas

En ocasiones una clase es una asociación consigo misma. En este caso la asociación se denomina asociación reflexiva. Esta situación se produce cuando una clase tiene objetos que pueden jugar diferentes roles. Por ejemplo, un ocupante de un avión de pasajeros puede ser: un pasajero, un miembro de tripulación o un piloto. Este tipo de asociación

P:221

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 625

se representa gráficamente dibujando la línea de asociación con origen y final en la propia clase y con indicación de

los roles y multiplicidades correspondientes.

EJEMPLO 17.5

1. Asociación reflexiva OcupanteAvión.

Piloto

OcupanteAvion

1..2

conduce Pasajero 0..250

Figura 17.23. Asociación reflexiva.

2. Asociación reflexiva OcupanteAuto.

Conductor

Auto

1 (Coche/Carro)

Ocupante 0..5

conduce

(maneja)

Figura 17.24. Asociación reflexiva.

17.3.7. Restricciones en asociaciones

Una asociación entre clases, a veces, tiene que seguir una regla determinada. Esta regla se indica poniendo una restricción cerca de la línea de la asociación. Una restricción típica se produce cuando una clase (un objeto) atiende a

otra clase (un objeto) en función de un determinado orden o secuencia. Por ejemplo, un vendedor de entradas de cine

(taquillero) atiende a los espectadores a medida que se sitúan delante de la ventanilla de entradas. En este caso, esta

restricción se representa en el modelo con la palabra ordered encerrada entre llaves.

Taquillero

de cine Espectador

atiende_a {ordered}

Figura 17.25. Restricciones entre asociaciones.

Cajero Cliente atiende_a {ordered}

Figura 17.26. Asociación con una restricción. (El cajero atiende al cliente por orden de llegada a caja.)

P:222

626 Fundamentos de programación

Otro tipo de restricciones se pueden presentar y se representan con relaciones or o bien xor, y se representan

gráficamente con una línea de asociación y las palabras or, xor entre llaves.

EJEMPLO 17.6

Un estudiante se matricula en una universidad en estudios de ingeniería o de ciencias.

AlumnoUniversidad {or}

Empresa

Empleado

se matricula

se matricula

Figura 17.27. Restricción en una asociación.

EJEMPLO 17.7

La relación xor implica una u otra asociación y no pueden ser nunca las dos. El caso de una póliza de seguro de

una empresa que puede ser o corporativa o de empleado, pero son entre sí, excluyentes.

Poliza {or}

Empresa

1..* 1..*

1..* Empleado

Figura 17.28. Relación xor en una asociación.

EJEMPLO 17.8

Un cajero de un banco (humano o electrónico) atiende a clientes. La atención a los clientes se realiza en el orden

en que se colocan ante la ventanilla o mostrador, o bien en función del momento de la petición electrónica de acceso al cajero.

Enlaces

Al igual que un objeto es una instancia de una clase, una asociación también tiene instancia. Por ejemplo la asociación Juega_en y su instancia se muestran en la Figura 17.29.

Jugador Equipo Juega_en

Una asociación

Figura 17.29. Instancia de una asociación.

17.4. AGREGACIÓN

Una agregación es un tipo especial de asociación que expresa un acoplamiento más fuerte entre clases. Una de las

clases juega un papel importante dentro de la relación con las otras clases. La agregación permite la representación

P:223

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 627

de relaciones tales como “maestro y esclavo”, “todo y parte de” o “compuesto y componentes”. Los componentes y

la clase que constituyen son una asociación que conforma un todo.

Las agregaciones representan conexiones bidireccionales y asimétricas. El concepto de agregación desde un punto de vista matemático es una relación que es transitiva, asimétrica y puede ser reflexiva.

Todo Parte

Figura 17.30. Relación de agregación.

La agregación es una versión más fuerte que la asociación. Al contrario que la asociación, la agregación implica

normalmente propiedad o pertenencia. La agregación se lee normalmente como relación “... posee un... “ o relación

“todo-parte”., en la cual una clase (“el todo”) representa un gran elemento que consta de elementos más pequeños

(“las partes”). La agregación se representa con un rombo a continuación de la clase “propietaria” y una línea recta

que apunta a la clase “poseída”. Esta relación se conoce como “tiene-un” ya que el todo tiene sus partes; un objeto

es parte de otro objeto.

Computadora

Unidad Central

de Proceso

(a)

Unidad

DVD

(b)

1 * Blogger Blogs

Teclado Pantalla

Figura 17.31. Relaciones de agregación: (a) Computadora con sus componentes;

(b) Un Blogger propietario de muchos (*) Blogs.

Desde el punto de vista conceptual una clase realmente posee, pero puede compartir objetos de otra clase. Una

agregación es un caso especial de asociación. Un ejemplo de una agregación es un automóvil que consta de cuatro

ruedas, un motor, un chasis, una caja de cambios, etc. Otro ejemplo es un árbol binario que consta de cero, uno o

dos nuevos árboles. Una agregación se representa como una jerarquía con la clase “todo” (por ejemplo, un sistema

de computadora) en la parte superior y sus componentes en las partes inferiores (por ejemplo CPU, discos, webcam,…). La representación de la agregación se realiza insertando un rombo vació en la parte todo.

EJEMPLO 17.9

Una computadora es un conjunto de elementos que consta de una unidad central, teclado, ratón, monitor, unidad de

CD-ROM, modém, altavoces, escáner, etc.

P:224

628 Fundamentos de programación

Unidad

Central Monitor Unidad

DVD/CD-RW Altavoces Escáner

1 11 41

1

Computadora

Figura 17.32. Una agregación computadora.

Restricciones en las agregaciones

En ocasiones el conjunto de componentes posibles en una agregación se establece dentro de una relación O. Así, por

ejemplo, el menú del día en un restaurante puede constar de: un primer plato (a elegir entre dos-tres platos), el segundo plato (a elegir entre dos-tres platos) y un postre (a elegir entre cuatro postres). El modelado de este tipo se

realiza con la palabra reservada O dentro de llaves con una línea discontinua que conecte las dos líneas que conforman el todo.

Primer

plato

Segundo

plato Postre

Plato 1 Plato 2

Menú

Postre 1 Postre 2 (O) (O)

Figura 17.33. Restricción en agregaciones.

17.4.1 Composición

Una composición es un tipo especial de agregación que impone algunas restricciones: si el objeto completo se copia o se borra (elimina), sus partes se copian o se suprimen con él. La composición representa una relación fuerte

entre clases y se utiliza para representar una relación todo-parte (whole-part). Cada componente dentro de una

composición puede pertenecer tan sólo a un todo. El símbolo de una composición es el mismo que el de una agregación, excepto que el rombo está relleno (Figura 17.34). Es como una agregación pero con el rombo pintado y no

vacío.

Tablero Patas

1

1

1 64

4

Tablero Casilla Mesa Comer

Figura 17.34. Relaciones de composición.

P:225

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 629

Una relación de composición se lee normalmente como “... es parte de...”, que significa se necesita leer la composición de la parte al todo. Por ejemplo, si una ventana de una página web tiene una barra de títulos, se puede representar que la clase BarraTitulo es parte de una clase denominada Ventana.

EJEMPLO 17.10

Una mesa para jugar al póker es una composición que consta de una superficie de la mesa y cuatro patas.

Superficie Patas

MesaDePoker

1 4

1

Figura 17.35. Composición

EJEMPLO 17.11

Un auto tiene un motor que no puede ser parte de otro auto. La eliminación completa del auto supone la eliminación

de su motor.

17.5. JERARQUÍA DE CLASES: GENERALIZACIÓN Y ESPECIALIZACIÓN

La jerarquía de clases (o clasificaciones) hace lo posible para gestionar la complejidad ordenando objetos dentro de

árboles de clases con niveles crecientes de abstracción. Las jerarquías de clases más conocidas son: generalización

y especialización.

La relación de generalización es un concepto fundamental de la programación orientada a objetos. Una generalización es una relación entre un elemento general (llamado superclase o “padre”) y un caso más concreto de ese

elemento (denominado subclase o “hijo”). Se conoce como relación es-un y tiene varios nombres, extensión, herencia... Las clases modelan el hecho de que el mundo real contiene objetos con propiedades y comportamientos. La

herencia modela el hecho de que estos objetos tienden a ser organizados en jerarquías. Estas jerarquías representan

la relación es-un.

La generalización normalmente se lee como “...es un...” comenzando en la clase específica, derivada o subclase

y derivándose a la superclase o clase base. Por ejemplo, un Gato es-un tipo de Animal. La relación de generali zación

se representa con una línea continua que comienza en la subclase y termina en una flecha cerrada en la superclase.

Animal

es-un

Gato

Figura 17.36. Relaciones de generalización.

En UML la relación se conoce como generalización y en programación orientada a objetos como herencia. Al

contrario que las relaciones de asociación, las relaciones de generalización no tienen nombre ni ningun tipo de multiplicidad.

P:226

630 Fundamentos de programación

Booch, para mostrar las semejanzas y diferencias entre clases, utiliza las siguientes clases de ob jetos: flores,

margaritas, rosas rojas, rosas amarillas y pétalos. Se puede constatar que: Una margarita es un tipo (una clase) de

flor.

• Una rosa es un tipo (diferente) de flor.

• Las rosas rojas y amarillas son tipos de rosas.

• Un pétalo es una parte de ambos tipos de flores.

Como Booch afirma, las clases y objetos no pueden existir aislados y, en consecuencia, existirán entre ellos relaciones. Las relaciones entre clases pueeden indicar alguna forma de compartición, así como algún tipo de conexión

semántica. Por ejemplo, las margaritas y las rosas son ambas tipos de flores, significando que ambas tienen pétalos

coloreados brillantemente, ambas emiten fragancia, etc. La conexión semántica se materializa en el hecho de que las

rosas rojas y las margaritas y las rosas están más estrechamente relacionadas entre sí que lo están los pétalos y

las flores.

Las clases se pueden organizar en estructuras jerárquicas. La herencia es una relación entre clases donde una

clase comparte la estructura o comportamiento, definida en una (herencia simple) o más clases (herencia múltiple).

Se denomina superclase a la clase de la cual heredan otras clases. De modo similar, una clase que hereda de una

o más clases se denomina subclase. Una subclase heredará atri butos de una superclase más elevada en el árbol

jerárquico. La herencia, por consiguiente, define un “tipo” de jerarquía entre clases, en las que una subclase hereda de una o más superclases. La Figura 17.37 ilustra una jerarquía de clases Animal con dos subclases que

heredan de Animal, Mamíferos y Reptiles.

Animal

Perro

Mamífero Reptil

Gato Zorro Cocodrilo Serpiente

Figura 17.37. Jerarquía de clases.

Herencia es la propiedad por la cual instancias de una clase hija (o subclase) puede acceder tanto a datos como

a comportamientos (métodos) asociados con una clase padre (o superclase). La heren cia siempre es transitiva, de

modo que una clase puede heredar características de superclases de nivel superior. Esto es, la clase Perro es una

subclase de la clase Mamífero y de Animal.

Una vez que una jerarquía se ha establecido es fácil extenderla. Para describir un nuevo concepto no es necesario

describir todos sus atributos. Basta describir sus diferencias a partir de un concepto de una jerarquía existente. La

herencia significa que el comportamiento y los datos asociados con las cla ses hija son siempre una extensión (esto

es, conjunto estrictamente más grande) de las propiedades asociadas con las clases padres. Una subclase debe tener

todas las propiedades de la clase padre y otras. El proceso de definir nuevos tipos y reutilizar código anteriormente

desarrollado en las definiciones de la clase base se denomina programación por herencia. Las clases que heredan

propiedades de una clase base pueden, a su vez, servir como clases base de otras clases. Esta jerarquía de tipos normalmente toma la estructura de árbol, conocido como jerarquía de clases o jerarquía de tipos.

La jerarquía de clases es un mecanismo muy eficiente, ya que se pueden utilizar definiciones de variables y métodos en más de una subclase sin duplicar sus definiciones. Por ejemplo, consideremos un sistema que representa

varias clases de vehículos manejados por humanos. Este sistema contendrá una clase genérica de vehículos, con subclases para todos los tipos especializados. La clase Vehículo contendrá los métodos y variables que fueran propios

de todos los vehículos, es decir, número de matrícula, número de pasajeros, capacidad del depósito de combustible.

La subclase, a su vez, con tendrá métodos y variables adicionales que serán específicos a casos individuales.

P:227

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 631

Camión Bus

Vehículo

Camioneta Remolque Trailer

Auto

Figura 17.38. Subclases de la clase Vehículo.

La flexibilidad y eficiencia de la herencia no es gratuita; se emplea tiempo en buscar una jerarquía de clases para

encontrar un método o variable, de modo que un programa orientado a objetos puede correr más lentamente que su

correspondiente convencional. Sin embargo, los diseñadores de lenguajes han desarrollado técnicas para eliminar esta

penalización en velocidad en la mayoría de los casos, permitiendo a las clases enlazar directamente con sus métodos

y variables heredados, de modo que no se requiera realmente ninguna búsqueda.

Vendedor

Empleado Estudiante

Secretario

Persona

Figura 17.39. Una jerarquía Persona.

Regla

• Cada objeto es una instancia de una clase.

• Algunas clases —abstractas— no pueden instanciar directamente.

• Cada enlace es una instancia de una asociación.

17.5.1. Jerarquías de generalización/especialización

Las clases con propiedades comunes se organizan en superclases. Una superclase representa una ge neralización

de las subclases. De igual modo, una subclase de una clase dada representa una especia lización de la clase superior (Figura 17.40). La clase derivada es-un tipo de clase de la clase base o superclase.

Una superclase representa una generalización de las subclases. Una sublcase de la clase dada re presenta una especialización de la clase ascendente (Figura 17.41).

En la modelización o modelado orientado a objetos es útil introducir clases en un cierto nivel que puede no existir en la realidad, pero que son construcciones conceptuales útiles. Estas clases se cono cen como clases abstractas

y su propiedad fundamental es que no se pueden crear instancias de ellas. Ejemplos de clases abstractas son ve hiculo

de pasajeros y vehiculo de mercancias. Por otra parte, de las subclases de estas clases abstractas, que corresponden a los objetos del mundo real, se pueden crear instancias directamente por sí mismas. Por ejemplo, de BMW se

pueden obtener, dos instancias, Coche1 y Coche2.

P:228

632 Fundamentos de programación

es-un es-un es-un es-un

es-un

es-un es-un

Vehículo

Bicicleta Ciclomotor Bus

Microbus Autobús Familiar Deportivo Lujo Motor 4 × 4

BMW Toyota

Coche

Camión

Tren

pasajeros

Tren de carga

Vehículo de pasajeros

Vehículo de pasajeros sin motor Vehículo de pasajeros con motor

Vehículo carga

Figura 17.40. Relaciones de gerneralización.

Generalización Especialización

Persona

Empleado

Informático

Programador

es-un

es-un

es-un

es-un

es-un

es-un

es-un

Mamífero

Hombre

Persona

Empleado

Oficinista

es-un

es-un

es-un

es-un

a) b) c)

BMW

Vehículo con motor

Vehículo

Coche

Deportivo

Figura 17.41. Relaciones de jerarquía es-un (is-a).

La generalización, en esencia, es una abstracción en que un conjunto de objetos de propiedades si milares se representa mediante un objeto genérico. El método usual para construir relaciones entre clases es definir generalizaciones buscando propiedades y funciones de un grupo de tipos de objetos similares, que se agrupan juntos para formar

un nuevo tipo genérico. Consideremos el caso de empleados de una compañía que pueden tener propiedades comunes

(nombre, número de empleado, dirección, etc.) y funcio nes comunes (calcular_nómina), aunque dichos empleados

pueden ser muy diferentes en atención a su trabajo: oficinistas, gerentes, programadores, ingenieros, etc. En este caso,

lo normal será crear un objeto genérico o superclase Empleado, que definirá una clase de empleados individuales.

P:229

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 633

Por ejemplo, Analistas, Programadores y Operadores se pueden generalizar en la clase informático. Un

pro gramador determinado (Mortimer) será miembro de las clases Programador, Informático y Em pleado; sin

embargo, los atributos significativos de este programador variarán de una clase a otra.

es-un es-un

es-un

es-un

es-un es-un

Empleado

Informático

Gerente

Analista Programador Operador

Técnico mantenimiento

Figura 17.42. Una jerarquía de generalización de empleados.

La jerarquía de generalización/especialización tiene dos características fundamentales y notables. Primero, un

tipo objeto no desciende más que de un tipo objeto genérico; segundo, los descendientes inmediatos de cualquier

nodo no necesitan ser objetos de clases exclusivas mutuamente. Por ejemplo, los Gerentes y los Informáticos

no tienen por qué ser exclusivos mutuamente, pero pueden ser tratados como dos objetos distintos; es el tipo de

relación que se denomina generalización múltiple.

es-un

es-un

es-un

es-un es-un

es-un

es-un

es-un

Técnico mantenimiento

Informático

Gerente

Analista Programador Operador Analista senior

Empleado

Figura 17.43. Una jerarquía de generalización múltiple.

UML define la generalización como herencia. De hecho, generalización es el concepto y herencia se considera

la implementación del concepto en un lenguaje de programación.

P:230

634 Fundamentos de programación

Síntesis de generalización/Especialización [Muller 97]

1. La generalización es una relación de herencia entre dos elementos de un modelo tal como clase. Permite a una clase heredar atributos y operaciones de otra clase. En realidad es la factorización de elementos

comunes (atributos operaciones y restricciones) dentro de un conjunto de clases en una clase más general denominada superclase. Las clases están ordenadas dentro de una jerarquía; una superclase es una

abstracción de sus subclases.

2. La flecha que representa la generalización entre dos clases apunta hacia la clase más general.

3. La especialización permite la captura de las características específicas de un conjunto de objetos que no

han sido distinguidos por las clases ya identificadas. Las nuevas características se representan por una

nueva clase, que es una subclase de una de las clases existentes. La especialización es una técnica muy

eficiente para extender un conjunto de clases de un modo coherente.

4. La generalización y la especialización son dos puntos de vista opuestos del concepto de jerarquía de

clasificación; expresan la dirección en que se extiende la jerarquía de clases.

5. Una generalización no lleva ningún nombre específico; siempre significa “es un tipo de”, “es un”, “es uno

de”, etc. La generalización sólo pertenece a clases, no se puede instanciar vía enlaces y por consiguiente no soporta el concepto de multiplicidad.

6. La generalización es una relación no reflexiva: una clase no se puede derivar de sí misma.

7. La generalización es una relación asimétrica: si la clase B se deriva de la clase A, entonces la clase A no

se puede derivar de la clase B.

8. La generalización es una relación transitiva: si la clase C se deriva de la clase B que a su vez se deriva

de la clase A, entonces la clase C se deriva de la clase A.

17.6. HERENCIA: CLASES DERIVADAS

Como ya se ha comentado, la herencia es la manifestación más clara de la relación de generalización/especialización

y a la vez una de las propiedades más importantes de la orientación a objetos y posiblemente su característica más

conocida y sobresaliente. Todos los lenguajes de programación orientados a objetos soportan directamente en su

propio lenguaje construcciones que implementan de modo directo la relación entre clases derivadas.

La herencia o relación es-un es la relación que existe entre dos clases, en la que una clase denominada derivada

se crea a partir de otra ya existente, denominada clase base. Este concepto nace de la necesidad de construir una

nueva clase y existe una clase que representa un concepto más general; en este caso la nueva clase puede heredar de

la clase ya existente. Así, por ejemplo, si existe una clase Figura y se desea crear una clase Triángulo, esta clase

Triángulo puede derivarse de Figura ya que tendrá en común con ella un estado y un comportamiento, aunque

luego tendrá sus características propias. Triángulo es-un tipo de Figura. Otro ejemplo, puede ser Programador

que es-un tipo de Empleado.

Empleado Figura

Programador Triángulo

Figura 17.44. Clases derivadas.

17.6.1. Herencia simple

La implementación de la generalización es la herencia. Una clase hija o subclase puede heredar atributos y operaciones de otra clase padre o superclase. La clase padre es más general que la clase hija. Una clase hija puede ser, a su

vez, una clase padre de otra clase hija. Mamífero es una clase derivada de Animal y Caballo es una clase hija o

derivada de Mamífero.

P:231

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 635

MamÍfero

Caballo

Anfibio Reptil

Animal

Figura 17.45. Herencia simple con dos niveles.

En UML, la herencia se representa con una línea que conecta la clase padre con la clase hija. En la parte de la línea

que conecta a la clase padre se pone un triángulo abierto (punta de flecha) que apunta a dicha clase padre. Este tipo de

conexión se representa como “es un tipo de”. Caballo es-un-tipo de Mamífero que a su vez es un tipo de Animal.

Una clase puede no tener padre, en cuyo caso se denomina clase base o clase raíz, y también puede no tener

ninguna clase hija, en cuyo caso se denomina clase terminal o clase hija.

Si una clase tiene exactamente un padre, tiene herencia simple. Si una clase tiene más de un padre, tiene herencia múltiple.

17.6.2. Herencia múltiple

Herencia múltiple o generalización múltiple —en terminología oficial de UML— se produce cuando una clase

hereda de dos o más clases padres (Figura 17.46).

Aunque la herencia múltiple está soportada en UML y en C++ (no en Java), en general, su uso no se considera

una buena práctica en la mayoría de los casos. Esta característica se debe al hecho de que la herencia múltiple presenta un problema complicado cuando las dos clases padre tienen solapamiento de atributos y comportamientos. ¿A qué

se debe la complicación? Normalmente a conflictos de atributos o propiedades derivadas. Por ejemplo, si las clases A1

y A2 tienen el mismo atributo nombre, la clase hija de ambas A1–2, ¿de cuál de las dos clases hereda el atributo?

C++, que soporta herencia múltiple, debe utilizar un conjunto propio de reglas del lenguaje C++ para resolver

estos conflictos. Estos problemas conducen a malas prácticas de diseño y ha hecho que lenguajes de programación

como Java y C# no soportan herencia múltiple. Sin embargo como C++ soporta esta característica, UML incluye en

sus representaciones este tipo de herencia.

Regla

La generalización es una relación “es-un” (un Carro es-un Vehiculo; un Gerente es-un Empleado, etc.). También

se utiliza “es-un-tipo-de” (is a kinf of).

En orientación a objetos la relación se conoce como herencia y en UML como generalización.

Clase A

Clase A1–2

Clase A1

nombre nombre

Clase A2

Figura 17.46. Herencia múltiple en la clase A1-2.

P:232

636 Fundamentos de programación

17.6.3. Niveles de herencia

La jerarquía de herencia puede tener más de dos niveles. Una clase hija puede ser una clase padre, a su vez, de otra

clase hija. Así, una clase Mamífero es una clase hija de Animal y una clase padre de Caballo.

Las clases hija o subclases añaden sus propios atributos y operaciones a los de sus clases base. Una clase puede

no tener clase hija, en cuyo caso es una clase hija. Si una clase tiene sólo un padre, se tiene herencia simple y si

tiene más de un padre, entonces, se tiene herencia múltiple.

Mamifero Anfibio Reptil

Animal

Caballo Rana Serpiente

Figura 17.47. Dos niveles en una jerarquía de herencia simple.

EJEMPLO 17.12

Representaciones gráficas de la herencia.

1. Vehículo es una superclase (clase base) y tiene como clase derivadas (subclase) Coche (Carro), Barco,

Avión y Camión. Se establece la jerarquía Vehículo, es una jerarquía generalización-especialización.

Vehículo

Coche

(carro) Barco Avión Camión

Figura 17.48. Diagrama de clases de la jerarquía Vehículo.

2. Jerarquía Vehículo (segunda representación gráfica, tipo árbol).

Vehículo

Coche

(carro) Barco Avión Camión

Figura 17.49. Jerarquía Vehículo en forma de árbol.

Evidentemente, la clase base y la clase derivada tienen código y datos comunes, de modo que si se crea la clase

derivada de modo independiente, se duplicaría mucho de lo que ya se ha escrito para la clase base. C++ soporta el

P:233

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 637

mecanismo de derivación que permite crear clases derivadas, de modo que la nueva clase hereda todos los miembros

datos y las funciones miembro que pertenecen a la clase ya existente.

La declaración de derivación de clases debe incluir el nombre de la clase base de la que se deriva y el especificador de acceso que indica el tipo de herencia (pública, privada y protegida). La primera línea de cada declaración

debe incluir el formato siguiente:

clase nombre_clase hereda_de tipo_herencia nombre_clase_base

Regla

En general, se debe incluir la palabra reservada publica en la primera línea de la declaración de la clase

derivada, y representa herencia pública. Esta palabra reservada produce que todos los miembros que son

públicos en la clase base permanecen públicos en la clase derivada.

EJEMPLO 17.13

Declaración de las clases Programador y Triangulo.

1. clase Programador hereda_de Empleado

publica:

// miembros públicos

privada:

// miembros privados

fin_clase

2. clase Triangulo hereda_de Figura

publica:

// sección pública

...

privado:

// sección privada

...

Una vez que se ha creado una clase derivada, el siguiente paso es añadir los nuevos miembros que se requieren

para cumplir las necesidades específicas de la nueva clase.

clase derivada clase base

clase Director hereda_de Empleado

publica:

nuevas funciones miembro

privada:

nuevos miembros dato

fin_clase

En la definición de la clase Director sólo se especifican los miembros nuevos (funciones y datos). Todas las

funciones miembro y los miembros dato de la clase Empleado son heredados automáticamente por la clase Director. Por ejemplo, la función calcular_salario de Empleado se aplica automáticamente a los directores:

Director d;

d.calcular_salario(325000);

P:234

638 Fundamentos de programación

EJEMPLO 17.14

Considérese una clase Prestamo y tres clases derivadas de ella: Pago_fijo, Pago_variable e Hipoteca.

Pago_fijo Pago_variable Hipoteca

Préstamo

clase Prestamo

protegida:

real capital;

real tasa_interes;

publica:

Prestamo(float, float);

virtual int crearTablaPagos(float[MAX_TERM][NUM_COLUMNAS] = 0;

fin_clase

Las variables capital tasa_interes no se repiten en la clase derivada

clase Pago_fijo hereda_de Prestamo

privada:

real pago; // cantidad mensual a pagar por cliente

publica:

Pago_Fijo (float, float, float);

ent CrearTablaPagos(float[MAX_TERM][NUM_COLUMNAS]);

};

clase Hipoteca hereda_de Prestamo

privada:

entero num_recibos;

entero recibos_por_anyo;

real pago;

publica:

Hipoteca(int, int, float, float, float);

entero CrearTablaPagos(float [MAX_TERN][NUM_COLUMNAS]);

fin_clase

17.6.4. Declaración de una clase derivada

La sintaxis para la declaración de una clase derivada es:

Especificador de acceso (normalmente público)

Nombre de la clase derivada

Tipo de herencia

Nombre de la clase base

clase ClaseDerivada hereda_de ClaseBase

publica:

// sección privada Símbolo de derivación o herencia

...

privada:

// sección privada

...

fin_clase

P:235

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 639

Especificador de acceso publica, significa que los miembros públicos de la clase base son miembros públicos

de la clase derivada.

Herencia pública, es aquella en que el especificador de acceso es publica (público).

Herencia privada, es aquella en que el especificador de acceso es privado (privado).

Herencia protegida, es aquella en que el especificador de acceso es protegida (protegido).

El especificador de acceso que declara el tipo de herencia es opcional (publica, privada o protegida); si se

omite el especificador de acceso, se considera por defecto privada. La clase base (ClaseBase) es el nombre de la

clase de la que se deriva la nueva clase. La lista de miembros consta de datos y funciones miembro:

clase nombre_clase hereda_de [especificador_acceso] ClaseBase

lista_de_miembros;

fin_clase

17.6.5. Consideraciones de diseño

A veces es difícil decidir cuál es la relación de herencia más óptima entre clases en el diseño de un programa. Consideremos, por ejemplo, el caso de los empleados o trabajadores de una empresa. Existen diferentes tipos de clasificaciones según el criterio de selección (se suele llamar discriminador) y pueden ser: modo de pago (sueldo fijo, por

horas, a comisión); dedicación a la empresa (plena o parcial) o estado de su relación laboral con la empresa (fijo o

temporal).

Una vista de los empleados basada en el modo de pago puede dividir a los empleados con salario mensual fijo;

empleados con pago por horas de trabajo y empleados a comisión por las ventas realizadas

Asalariado A comisión Por horas

Empleado

Una vista de los empleados basada en el estado de dedicación a la empresa: dedicación plena o dedicación

parcial.

Dedicación_Plena Dedicación_Parcial

Empleado

Una vista de empleados basada en el estado laboral del empleado con la empresa: fijo o temporal.

Empleado

Fijo Temporal

Una dificultad a la que suele enfrentarse el diseñador es que en los casos anteriores un mismo empleado puede pertenecer a diferentes grupos de trabajadores. Un empleado con dedicación plena puede ser remunerado con un salario

mensual. Un empleado con dedicación parcial puede ser remunerado mediante comisiones y un empleado fijo puede

P:236

640 Fundamentos de programación

ser remunerado por horas. Una pregunta usual es ¿cuál es la relación de herencia que describe la mayor cantidad de

variación en los atributos de las clases y operaciones?, ¿esta relación ha de ser el fundamento del diseño de clases?

Evidentemente la respuesta adecuada sólo se podrá dar cuando se tenga presente la aplicación real a desarrollar.

17.7. ACCESIBILIDAD Y VISIBILIDAD EN HERENCIA

En una clase existen secciones públicas, privadas y protegidas. Los elementos públicos son accesibles a todas las

funciones; los elementos privados son accesibles sólo a los miembros de la clase en que están definidos y los elementos protegidos pueden ser accedidos por clases derivadas debido a la propiedad de la herencia. En correspondencia con lo anterior existen tres tipos de herencia: pública, privada y protegida. Normalmente el tipo de herencia más

utilizada es la herencia pública.

Con independencia del tipo de herencia, una clase derivada no puede acceder a variables y funciones privadas de

su clase base. Para ocultar los detalles de la clase base y de clases y funciones externas a la jerarquía de clases, una

clase base utiliza normalmente elementos protegidos en lugar de elementos privados. Suponiendo herencia pública,

los elementos protegidos son accesibles a las funciones miembro de todas las clases derivadas.

Tabla 17.2. Acceso a variables y funciones según tipo de herencia

Tipo de herencia Tipo de elemento ¿Accesible a clase derivada?

Publica publica

protegida

privada

no

Privada publica

protegida

privada

no

no

no

Norma

Por defecto, la herencia es privada. Si accidentalmente se olvida la palabra reservada publica, los elementos de

la clase base serán inaccesibles. El tipo de herencia es, por consiguiente, una de las primeras cosas que se debe

verificar si un compilador devuelve un mensaje de error que indique que las variables o funciones son inaccesibles.

17.7.1. Herencia pública

En general, herencia pública significa que una clase derivada tiene acceso a los elementos públicos y privados de su

clase base. Los elementos públicos se heredan como elementos públicos; los elementos protegidos permanecen protegidos. La herencia pública se representa con el especificador publica en la derivación de clases.

Formato

clase ClaseDerivada hereda_de publica Clase Base

publica:

// sección pública

privada:

// sección privada

fin_clase

17.7.2. Herencia privada

La herencia privada significa que una clase derivada no tiene acceso a ninguno de sus elementos de la clase base. El

formato es:

P:237

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 641

clase ClaseDerivada hereda_de privada ClaseBase

publica:

// sección pública

protegida:

// sección protegida

privada:

// sección privada

fin_clase

Con herencia privada, los miembros públicos y protegidos de la clase base se vuelven miembros privados de la

clase derivada. En efecto, los usuarios de la clase derivada no tienen acceso a las facilidades proporcionadas por la

clase base. Los miembros privados de la clase base son inaccesibles a las funciones miembro de la clase derivada.

La herencia privada se utiliza con menos frecuencia que la herencia pública. Este tipo de herencia oculta la clase

base del usuario y así es posible cambiar la implementación de la clase base o eliminarla toda junta sin requerir ningún cambio al usuario de la interfaz. Cuando un especificador de acceso no está presente en la declaración de una

clase derivada, se utiliza herencia privada.

17.7.3. Herencia protegida

Con herencia protegida, los miembros públicos y protegidos de la clase base se convierten en miembros protegidos

de la clase derivada y los miembros privados de la clase base se vuelven inaccesibles. La herencia protegida es apropiada cuando las facilidades o aptitudes de la clase base son útiles en la implementación de la clase derivada, pero

no son parte de la interfaz que el usuario de la clase ve. La herencia protegida es todavía menos frecuente que la

herencia privada.

Tabla 17.3. Tipos de herencia y accesos que permiten

Tipo de herencia Acceso a miembro clase base Acceso a miembro clase derivada

publica publica

protegida

privada

publica

protegida

inaccesible

protegida publica

protegida

privada

protegida

protegida

inaccesible

privada publica

protegida

privada

privada

privada

inaccesible

La Tabla 17.3 resume los efectos de los tres tipos de herencia en la accesibilidad de los miembros de la clase

derivada. La entrada inaccesible indica que la clase derivada no tiene acceso al miembro de la clase base.

EJERCICIO 17.3

Declarar una clase base (Base) y tres clases derivadas de ella, D1,D2 y D3

clase Base {

publica:

entero i1;

protegida:

entero i2;

privada:

entero i3;

};

P:238

642 Fundamentos de programación

clase D1: privada Base {

nada f();

};

clase D2: protegida Base {

nada g();

};

clase D3: publica Base {

nada h();

};

Ninguna de las subclases tiene acceso al miembro i3 de la clase Base. Las tres clases pueden acceder a los

miembros i1 e i2. En la definición de la función miembro f() se tiene:

void D1::f() {

i1 = 0; // Correcto

i2 = 0; // Correcto

i3 = 0; // Error

};

17.8. UN CASO DE ESTUDIO ESPECIAL: HERENCIA MÚLTIPLE

Herencia múltiple es un tipo de herencia en la que una clase hereda el estado (estructura) y el comportamiento de

más de una clase base. En otras palabras hay herencia múltiple cuando una clase hereda de más de una clase; es decir, existen múltiples clases base (ascendientes o padres) para la clase derivada (descendiente o hija).

La herencia múltiple entraña un concepto más complicado que la herencia simple, no sólo con respecto a la

sintaxis sino también al diseño e implementación del compilador. La herencia múltiple también aumenta las operaciones auxiliares y complementarias y produce ambigüedades potenciales. Además, el diseño con clases derivadas

por derivación múltiple tiende a producir más clases que el diseño con herencia simple. Sin embargo, y pese a los

inconvenientes y ser un tema controvertido, la herencia múltiple puede simplificar los programas y proporcionar

soluciones para resolver problemas difíciles. En la Figura 17.50 se muestran diferentes ejemplos de herencia múltiple.

Estudiante

Profesor

ayudante

Estudiante

Doctorado

Persona

Profesor Farola

Poste Luz

Hidroavión

Barco Avión

Figura 17.50. Ejemplos de herencia múltiple.

Regla

En herencia simple, una clase derivada hereda exactamente de una clase base (tiene sólo un padre). Herencia

múltiple implica múltiples clases base (tiene varios padres una clase derivada).

P:239

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 643

En herencia simple, el escenario es bastante sencillo, en términos de concepto y de implementación. En herencia

múltiple los escenarios varían ya que las clases base pueden proceder de diferentes sistemas y se requiere a la hora

de la implementación un compilador de un lenguaje que soporte dicho tipo de herencia (C++ o Eiffel). ¿Por qué

utilizar herencia múltiple? Pensamos que la herencia múltiple añade fortaleza a los programas y si se tiene precaución

en la base del análisis y posterior diseño, ayuda bastante a la resolución de muchos problemas que tomen naturaleza

de herencia múltiple.

Por otra parte, la herencia múltiple siempre se puede eliminar y convertirla en herencia simple si el lenguaje de

implementación no la soporta o considera que tendrá dificultades en etapas posteriores a la implementación real. La

sintaxis de la herencia múltiple es:

clase CDerivada hereda_de Base1, Base2,...

publica:

// sección pública

privada:

// sección privada

...

fin_clase

CDerivada Nombre de la clase derivada

Base1, Base2,... Clases base con nombres diferentes

Funciones o datos miembro que tengan el mismo nombre en Base1, Base2, Basen,... serán motivo de ambigüedad.

clase A hereda_de publica B, C {...}

clase D hereda_de publica E, publica F, publica G {...}

La palabra reservada publica ya se ha comentado anteriormente, define la relación “es-un” y crea un subtipo

para herencia simple. Así en los ejemplos anteriores, la clase A “es-un” tipo de B y “es-un” tipo de C. La clase D se

deriva públicamente de E y G y privadamente de F. Esta derivación hace a D un subtipo de E y G pero no un subtipo

de F. El tipo de acceso sólo se aplica a una clase base.

clase Derivada hereda_de publica Base1, Base2 {...};

Derivada especifica derivación pública de Base1 y derivación privada (por defecto u omisión) de Base2,

Regla

Asegúrese de especificar un tipo de acceso en todas las clases base para evitar el acceso privado por omisión.

Utilice explícitamente privada cuando lo necesite para manejar la legibilidad.

Class Derivada: publica Base1, privada Base2 {...}

EJEMPLO 17.15

clase Estudiante {

...

};

clase Trabajador {

...

};

clase Estudiante_Trabajador: publica Estudiante, publica Trabajador {

...

};

P:240

644 Fundamentos de programación

17.8.1. Características de la herencia múltiple

La herencia múltiple plantea diferentes problemas tales como la ambigüedad por el uso de nombres idénticos en

diferentes clases base, y la dominación o preponderancia de funciones o datos.

Ambigüedades

Al contrario que la herencia simple, la herencia múltiple tiene el problema potencial de las ambigüedades.

EJEMPLO 17.16

clase Ventana {

privada:

...

publica:

nada dimensionar(); // dimensiona una ventana

...

fin_clase

clase Fuente {

privada:

...

publica:

nada dimensionar(); // dimensiona un tipo fuente

...

fin_clase

Una clase Ventana tiene una función dimensionar() que cambia el tamaño de la ventana; de modo similar,

una clase Fuente modifica los objetos Fuente con dimensionar(). Si se crea una clase Ventana_Fuente

(VFuente) con herencia múltiple, se puede producir ambigüedad en el uso de dimensionar()

clase VFuente: publica Ventana, publica Fuente {...};

VFuente v;

v.dimensionar(); // se produce un error ¿cuál?

La llamada a dimensionar es ambigua, ya que el compilador no sabrá a qué función dimensionar ha de llamar.

Esta ambigüedad se resuelve fácilmente con el operador de resolución de ámbito (:: )

v.Fuente::dimensionar(); // llamada a dimensionar() de Fuente

v.Ventana::dimensionar(); // llamada a dimensionar de Ventana

Precaución

No es un error definir un objeto derivado con multiplicidad con ambigüedades. Estas se consideran ambigüedades

potenciales y sólo produce errores en tiempo de compilación cuando se llaman de modo ambiguo.

Regla

Incluso es mejor solución que la citada anteriormente resolver la ambigüedad en las propias definiciones de la

función dimensionar()

clase VFuente: publica Ventana, publica Fuente

...

void v_dimensionar() { Ventana::dimensionar(); }

void f_dimensionar() { Fuente::dimensionar(); }

fin_clase

P:241

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 645

EJEMPLO 17.17

Diseñar e implementar una jerarquía de clases que represente las relaciones entre las clases siguientes: estudiante,

empleado, empleado asalariado y un estudiante de doctorado que es a su vez profesor de prácticas de laboratorio.

Asalariado

Estudiante Empleado

Estudiante

Doctorado

Profesor

Nota

Se deja la resolución como ejercicio al lector.

17.9. CLASES ABSTRACTAS

Una clase abstracta es una clase que no tiene ningún objeto; o con mayor precisión, es una clase que no puede tener

objetos instancias de la clase base. Una clase abstracta describe atributos y comportamientos comunes a otras clases,

y deja algunos aspectos del funcionamiento de la clase a las subclases concretas. Una clase abstracta se representa

con su nombre en cursiva.

EJERCICIO 17.4

Clase abstracta Vehículo con clases derivadas Coche(Carro) y Barco.

Coche

deportivo

Coche

de pasajero Taxi

Coche

(carro)

Yate Canoa Lancha

Barco

Vehículo

Figura 17.51. Jerarquía con clase base abstracta Vehículo.

Una clase abstracta se representa poniendo su nombre en cursiva o añadiendo la palabra {abstract} dentro del

compartimento de la clase y debajo del nombre de la clase.

P:242

646 Fundamentos de programación

EJERCICIO 17.5

Clase abstracta Futbolista de la cual derivan las clases concretas Portero, Defensa y Delantero.

Futbolista

nombre

edad

altura

nacionalidad

número

correr()

driblar()

regatear()

lanzar()

Portero

parar()

sacar()

pararPenalty()

Defensa

quitarBalon()

defender()

pararBalon()

Delantero

tirarPuerta()

lanzarPenalty()

lanzarFalta()

Figura 17.52. Jerarquía con clase base abstracta Futbolista.

17.9.1. Operaciones abstractas

Una clase abstracta tiene operaciones abstractas. Una operación abstracta no tiene implementación de métodos, sólo

la signatura o prototipo. Una clase que tiene al menos una operación abstracta es, por definición, abstracta.

Una clase que hereda de una clase que tiene una o más operaciones abstractas debe implementar esas operaciones (proporcionar métodos para esas operaciones). Las operaciones abstractas se muestran con la cadena

{abstract} a continuación del prototipo o signatura de la clase. Las operaciones abstractas se definen en las

clases abstractas para especificar el comportamiento que deben tener todas las subclases. Una clase Vehículo

debe tener operaciones abstractas que especifiquen comportamientos comunes de todos los vehículos (conducir,

frenar, arrancar...).

Los modeladores suelen proporcionar siempre una capa de clases abstractas como superclases, buscando elementos comunes a cualquier relación de herencia que se puede extender a las clases hijas. En el ejemplo de las clases

abstractas, Coche y Barco representan a clases que requieren implementar las operaciones abstractas “conducir” y

“frenar”.

Una clase concreta es una clase opuesta a la clase abstracta. En una clase concreta, es posible crear objetos de la

clase que tienen implementaciones de todas las operaciones. Si la clase Vehículo tiene especificada una operación

abstracta conducir, tanto las clases Coche como Barco deben implementar ese método (o las propias operaciones

deben ser especificadas como abstractas). Sin embargo, las implementaciones son diferentes. En un coche, la operación conducir hace que las ruedas se muevan; mientras que conducir un barco hace que el barco navegue (se mueva).

Las subclases heredan operaciones de una superclase común, pero dichas operaciones se implementan de modo diferente.

Una subclase puede redefinir (modificar el comportamiento de la superclase) las operaciones de la superclase, o

bien implementan la superclase tal y como está definida. Una operación redefinida debe tener la misma signatura o

prototipo (tipo de retorno, nombre y parámetros) que la superclase. La operación que se está redefiniendo puede ser

o bien abstracta (no tiene implementación en la superclase) o concreta (tiene una implementación en la superclase).

En cualquier caso, la redefinición en las subclases se utiliza para todas las instancias de esa clase.

P:243

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 647

Se pueden añadir a las subclases nuevas operaciones, atributos y asociaciones. Un objeto de una subclase se

puede utilizar en cualquier situación donde sea posible utilizar objetos de la superclase. En ese caso, la subclase

tendrá una implementación diferente dependiendo del objeto implicado.

color

año de fabricación

conducir {abstracto}

Coche (carro)

conducir()

conducir()

mueve

las ruedas

Barco

conducir()

conducir()

mueve el barco

y los propulsores

Vehículo

{abstracto}

Figura 17.53. La clase Vehículo (abstracta) hereda los atributos color

y añoDeFabricación, y la operación conducir.

RESUMEN

Una asociación es una conexión semántica entre clases. Una

asociación permite que una clase conozca de los atributos

y operaciones públicas de otra clase.

Una agregación es una relación más fuerte que una asociación y representa una clase que se compone de otras

clases. Una agregacion representa la relación todo-parte; es

decir una clase es el todo y contiene a todas las partes.

Una generalización es una relación de herencia entre

dos elementos de un modelo tal como clases. Permite a una

clase heredar atributos y operaciones de otra clase. Su implementación en un lenguaje orientado a objetos es la herencia. La especialización es la relación opuesta a la generalización.

La relación es-un representa la herencia. Por ejemplo,

una rosa es un tipo de flor; un pastor alemán es un tipo de

perro, etc. La relación es-un es transitiva. Un pastor alemán

es un tipo de perro y un perro es un tipo de mamífero; por

consiguiente, un pastor alemán es un mamífero. Una clase

CONCEPTOS CLAVE

• Agregación.

• Asociación.

• Clase abstracta.

• Clase base.

• Clase derivada.

• Composición.

• Constructor.

• Declaración de acceso.

• Destructor.

• Especificadores de acceso.

• Función virtual.

• Generalización.

• Herencia.

• Herencia múltiple.

• Herencia protegida.

• Herencia pública y privada.

• Herencia simple.

• Ligadura dinámica.

• Ligadura estática.

• Multiplicidad.

• Polimorfismo.

• Relación es-un.

• Relación todo-parte.

P:244

648 Fundamentos de programación

nueva que se crea a partir de una clase ya existente, utilizando herencia, se denomina clase derivada o subclase. La

clase padre se denomina clase base o superclase.

1. Herencia es la capacidad de derivar una clase de

otra clase. La clase inicial utilizada por la clase

derivada se conoce como clase base, padre o superclase. La clase derivada se conoce como derivada, hija o subclase.

2. Herencia simple es la relación entre clases que se

produce cuando una nueva clase se crea utilizando las propiedades de una clase ya existente. Las

relaciones de herencia reducen código redundante en programas. Uno de los requisitos para que

un lenguaje sea considerado orientado a objetos

es que soporte herencia.

3. La herencia múltiple se produce cuando una clase

se deriva de dos o más clases base. Aunque es una

herramienta potente, puede crear problemas, especialmente de colisión o conflicto de nombres, cosa

que se produce cuando nombres idénticos aparecen en más de una clase base.

EJERCICIOS

17.1. Definir una clase base Persona que contenga información de propósito general común a todas las personas (nombre, dirección, fecha de nacimiento, sexo,

etc.). Diseñar una jerarquía de clases que contemple

las clases siguientes: Estudiante, Empleado, Estudiante_empleado. Escribir un programa que lea

un archivo de información y cree una lista de personas: a) general; b) estudiantes; c) empleados; d) estudiantes empleados. El programa debe permitir ordenar alfabéticamente por el primer apellido.

17.2. Implementar una jerarquía Librería que tenga al

menos una docena de clases. Considérese una librería que tenga colecciones de libros de literatura, humanidades, tecnología, etc.

17.3. Diseñar una jerarquía de clases que utilice como clase base o raíz una clase LAN (red de área local).

Las subclases derivadas deben representar diferentes

topologías, como estrella, anillo, bus y hub. Los

miembros datos deben representar propiedades tales

como soporte de transmisión, control de acceso, formato del marco de datos, estándares, velocidad de

transmisión, etc. Se desea simular la actividad de

los nodos de tal LAN.

La red consta de nodos, que pueden ser dispositivos tales como computadoras personales, estaciones

de trabajo, máquinas FAX, etc. Una tarea principal

de LAN es soportar comunicaciones de datos entre

sus nodos. El usuario del proceso de simulación

debe, como mínimo, poder:

• Enumerar los nodos actuales de la red LAN.

• Añadir un nuevo nodo a la red LAN.

• Quitar un nodo de la red LAN.

• Configurar la red, proporcionándole una topología

de estrella o en bus.

• Especificar el tamaño del paquete, que es el tamaño en bytes del mensaje que va de un nodo a otro.

• Enviar un paquete de un nodo especificado a otro.

• Difundir un paquete desde un nodo a todos los

demás de la red.

• Realizar estadísticas de la LAN, tales como tiempo

medio que emplea un paquete.

17.4. Implementar una jerarquía de cualquier

tipo de empresa que le sea familliar. La jerarquía

debe tener al menos cuatro niveles, con herencia de

miembros dato, y métodos. Los métodos deben poder calcular salarios, despidos, promoción, dar de

alta, jubilación, etc. Los métodos deben permitir

también calcular aumentos salariales y primas para

Empleados de acuerdo con su categoría y productividad. La jerarquía de herencia debe poder ser utilizada para proporcionar diferentes tipos de acceso

a Empleados. Por ejemplo, el tipo de acceso garantizado al público diferirá del tipo de aceso proporcionado a un supervisor de empleado, al departamento de nóminas, o al Ministerio de Hacienda.

Utilice la herencia para distinguir entre al menos

cuatro tipos diferentes de acceso a la información de

Empleado.

17.5. Implementar una clase Automovil (Carro) dentre

de una jerarquía de herencia múltiple. Considere

que, además de ser un Vehículo, un automóvil es

también una comodidad, un símbolo de estado social, un modo de transporte, etc. Automovil debe

tener al menos tres clases base y al menos tres clases

derivadas.

17.6. Escribir una clase FigGeometrica que represente

figuras geométricas tales como punto, línea, rectángulo, triángulo y similares. Debe proporcionar métodos que permitan dibujar, ampliar, mover y destruir

tales objetos. La jerarquía debe constar al menos de

una docena de clases.

17.7. Implementar una jerarquía de tipos de datos numéricos que extienda los tipos de datos fundamentales

Empleado

P:245

Relaciones entre clases: Delegaciones, asociaciones, agregaciones, herencia 649

tales como int y float, disponibles en C++. Las

clases a diseñar pueden ser Complejo, Fracción,

Vector, Matriz, etc.

17.8. Implementar una jerarquía de herencia de animales

tal que contenga al menos seis niveles de derivación y doce clases.

17.9. Diseñar la siguiente jerarquía de clases:

Persona

Nombre

edad

visualizar()

Estudiante Profesor

nombre

edad

id

visualizar()

heredado

heredado

definido

redefinido

nombre

edad

salario

visualizar()

heredado

heredado

definido

heredada

Escribir un programa que manipule la jerarquía

de clases, lea un objeto de cada clase y lo visualice.

17.10. Crear una clase base denominada Punto que conste de las coordenadas x e y. A partir de esta clase,

definir una clase denominada Circulo que tenga

las coordenada del centro y un atributo denominado radio. Entre las funciones miembro de la primera clase, deberá existir una función distancia( ) que devuelva la distancia entre dos puntos,

donde:

Distancia = ((x2 – x1)2 + (y2 – y1)2

)

1/2

17.11. Utilizando la clase construida en el Ejercicio 17.10

obtener una clase derivada Cilindro derivada de

Circulo. La clase Cilindro deberá tener una

función miembro que calcule la superficie de dicho

cilindro. La fórmula que calcula la superficie del cilindro es S = 2r(l + r) donde r es el radio del cilindro y l es la longitud.

17.12. Crear una clase base denominada Rectangulo

que contenga como miembros datos, longitud y anchura. De esta clase, derivar una clase denominada

Caja que tenga un miembro adicional denominado

profundidad y otra función miembro que permita

calcular su volumen.

17.13. Dibujar un diagrama de objetos que represente la

estructura de un coche (carro). Indicar las posibles

relaciones de asociación, generalización y agregación.

P:247

651 Fundamentos de programación

PARTE IV

Metodología

de la programación

y desarrollo de software

CONTENIDO

Capítulo 18. Resolución de problemas y desarrollo de software: Metodología de la

programación

P:249

CAPÍTULO 18

Resolución de problemas

y desarrollo de software:

Metodología de la programación

18.1. Abstracción y resolución de problemas

18.2. El ciclo de vida del software

18.3. Fase de análisis: requisitos y especificaciones

18.4. Diseño

18.5. Implementación (codificación)

18.6. Pruebas e integración

18.7. Mantenimiento

18.8. Principios de diseño de sistemas de software

18.9. Estilo de programación

18.10. La documentación

18.11. Depuración

18.12. Diseño de algoritmos

18.13. Pruebas (testing)

18.14. Eficiencia

18.15. Transportabilidad

CONCEPTOS CLAVE

RESUMEN

La producción de un programa se puede dividir en

diferentes fases: análisis, diseño, codificación y depuración, prueba y mantenimiento. Estas fases se conocen como ciclo de vida del software, y son los principios básicos en los que se apoya la ingeniería del

software. Debe considerarse siempre todas las fases

en el proceso de creación de programas, sobre todo

cuando éstos son grandes proyectos. La ingeniería del

software trata de la creación y producción de programas a gran escala.

INTRODUCCIÓN

P:250

654 Fundamentos de programación

18.1. ABSTRACCIÓN Y RESOLUCIÓN DE PROBLEMAS

Los seres humanos se han convertido en la especie más influyente de este planeta, debido a su capacidad para abstraer

el pensamiento. Los sistemas complejos, sean naturales o artificiales, sólo pueden ser comprendidos y gestionados

cuando se omiten detalles que son irrelevantes a nuestras necesidades inmediatas. El proceso de excluir detalles no

deseados o no significativos al problema que se trata de resolver se denomina abstracción, y es algo que se hace en

cualquier momento.

Cualquier sistema de complejidad suficiente se puede visualizar en diversos niveles de abstracción dependiendo

del propósito del problema. Si nuestra intención es conseguir una visión general del proceso, las características del

proceso presente en nuestra abstracción constará principalmente de generalizaciones. Sin embargo, si se trata de modificar partes de un sistema, se necesitará examinar esas partes con gran nivel de detalle. Consideremos el problema

de representar un sistema relativamente complejo, tal como un coche. El nivel de abstracción será diferente según

sea la persona o entidad que se relaciona con el coche: conductor, propietario, fabricante o mecánica.

Así, desde el punto de vista del conductor sus características se expresan en términos de sus funciones (acelerar,

frenar, conducir, etc.); desde el punto de vista del propietario sus características se expresan en función de nombre,

dirección, edad; la mecánica del coche es una colección de partes que cooperan entre sí para proveer las funciones

citadas, mientras que desde el punto de vista del fabricante interesa precio, producción anual de la empresa, duración

de construcción, etc. La existencia de diferentes niveles de abstracción conduce a la idea de una jerarquía de abstracciones.

Una abstracción es un modelo de una entidad física o actividad. Así, se puede utilizar la abstracción para desarrollar modelos de entidades (objetos y clases) y también las operaciones ejecutadas sobre esos objetos. Un

ejemplo de abstracción es el uso de una variable de programa (por ejemplo, salario o ciudad) para representar

una posición de almacenamiento en memoria para un valor de un dato. No necesita estar enterado de los detalles

de la estructura física de la memoria o de los bits reales que utilizan para representar el valor de una variable ni

para utilizar esa variable en un programa. Esta característica es análoga a conducir (manejar) un automóvil. El

conductor necesita conocer cómo utilizar la llave para arrancar el motor, cómo utilizar los pedales de frenos y

acelerador, cómo controlar la velocidad y como utilizar el volante para controlar la dirección. Sin embargo, el

conductor no necesita conocer los detalles del sistema eléctrico del coche (carro), del sistema de frenos o del tren

de rodaje.

Las soluciones a problemas no triviales tiene una jerarquía de abstracciones de modo que sólo los objetivos generales son evidentes al nivel más alto. A medida que se desciende en nivel los aspectos diferentes de la solución se

hacen evidentes.

En un intento de controlar la complejidad, los diseñadores del sistema explotan las características bidimensionales de la jerarquía de abstracciones. La primera etapa al tratar con un problema grande es seleccionar un nivel apropiado a las herramientas (hardware y software) que se utilizan para resolverlo. El problema se descompone entonces

en subproblemas, que se pueden resolver independientemente de modo razonable.

El término resolución del problema se refiere al proceso completo de tomar la descripción del problema y desarrollar un programa de computadora que resuelva ese problema. Este proceso requiere pasar a través de muchas

fases, desde una buena comprensión del problema a resolver hasta el diseño de una solución conceptual, para implementar la solución con un programa de computadora.

Realmente ¿qué es una solución? Normalmente, una solución consta de dos componentes: algoritmos y medios

para almacenar datos. Un algoritmo es una especificación concisa de un método para resolver un problema. Una

acción que un algoritmo realiza con frecuencia es operar sobre una colección de datos. Por ejemplo, un algoritmo

puede tener que poner menos datos en una colección, quitar datos de una colección o realizar preguntas sobre una

colección de datos. Cuando se construye una solución, se deben organizar sus colecciones de datos de modo que se

pueda operar sobre los datos fácilmente en la manera que requiera el algoritmo. Sin embargo, no sólo se necesita

almacenar los datos en estructuras de datos sino también operar sobre esos datos.

Diferentes herramientas ayudan al programador y al ingeniero de software a diseñar una solución para un problema dado. Algunas de estas herramientas son diseño descendente, abstracción procedimental, abstracción de datos,

ocultación de la información, recursión o recursividad y programación orientada a objetos.

18.1.1. Descomposición procedimental

Con la introducción de los módulos (procedimientos, funciones o métodos en programación orientada a objetos) se

vio que cada programa constaba de diversos módulos que se llaman entre sí en secuencia. Un módulo principal llama

P:251

Resolución de problemas y desarrollo de software: Metodología de la programación 655

a otro módulo, estos módulos a otros módulos y así sucesivamente. La estrategia que se ha seguido en todo el libro

ha sido la descomposición procedimental o refinamiento sucesivo.

Cuando se construye un programa para resolver un problema, el problema completo se moldea, en primer lugar,

como un único procedimiento. Este procedimiento de nivel superior se define entonces en términos de llamadas a

otros procedimientos, que a su vez se definen en términos de otros procedimientos creando una jerarquía de procedimientos. Este proceso continúa hasta que se alcanza una colección de procedimientos que ya no necesitan más

refinamiento dado que los mismos se construyen totalmente en términos de sentencias en el lenguaje algorítmico

(o mejor en el lenguaje de programación elegido). Esta es la razón de denominar a este método “refinamiento sucesivo” o “refinamiento descendente top-down”. Entonces un programa completo se puede componer de un programa

principal (main) y de otros procedimientos (Figura 18.1). Cuando se ejecuta el programa, main llama a P2 que a su

vez llama a P1 y el control se devuelve a main que en la siguiente llamada invoca a P3 y P3 llama a P4 seguido por

P1. Este grafo se suele denominar grafo de llamadas y se utiliza para mostrar qué procedimientos se invocan y cómo

se llaman a su vez entre sí estos procedimientos. Estos grafos suelen ayudar al programador a deducir el comportamiento del programa y su estructura a un nivel más abstracto.

Función P1

Programa principal

Función F Procedimiento P2

Figura 18.1. Un programa dividido en módulos independientes.

El refinamiento descendente utiliza los procedimientos como base de lo que se conoce como programación estructurada. Este estilo de programación fue muy popular en la década de los sesenta y setenta e incluso en los

ochenta, y aunque todavía se sigue utilizando, está siendo sustituido cada vez con mayor frecuencia por la programación orientada a objetos. Los lenguajes representativos, por excelencia, de la programación estructurada son

Pascal y C. El desarrollo de programas estructurados era una progresión bastante natural de la tecnología y metodología de la programación y se hizo muy popular. Sin embargo, el concepto de tipos abstractos de datos y objetos ha

hecho que la programación se haya desplazado hacia la programación orientada a objetos. Este nuevo paradigma

condujo de modo masivo a la aceptación del lenguaje C++ y en la segunda mitad de la década de los noventa a Java

y en los primeros años del siglo XXI al nuevo lenguaje estrella de Microsoft, C#.

Aunque es posible escribir programas C++ o Java diseñados utilizando refinamiento descendente, el lenguaje

realmente no está diseñado para soportar este estilo y es mejor utilizar el enfoque orientado a objetos.

18.1.2. Diseño descendente

Cuando se escriben programas de tamaño y complejidad moderada, nos enfrentamos a la dificultad de escribir dichos

programas. La solución para resolver estos problemas y, naturalmente, aquellos de mayor tamaño y complejidad, es

recurrir a la modularidad mediante el diseño descendente. ¿Qué significa diseño descendente y modularidad? La

filosofía del diseño descendente reside en que se descompone una tarea en sucesivos niveles de detalle.

Para ello se divide el programa en módulos independientes —procedimientos, funciones y otros bloques de código— como se observa en la Figura 18.1.

El concepto de solución modular se aprecia en la aplicación de la Figura 18.2, que busca encontrar la nota media

de un conjunto de notas de una clase de informática. Existe un módulo del más alto nivel que se va refinando en

sentido descendente para encontrar módulos adicionales más pequeños. El resultado es una jerarquía de módulos;

cada módulo se refina por los de bajo nivel que resuelve problemas más pequeños y contiene más detalles sobre los

mismos. El proceso de refinamiento continúa hasta que los módulos de nivel inferior de la jerarquía sean tan simples

como para traducirlos directamente a procedimientos, funciones y bloques de código que resuelven problemas independientes muy pequeños. De hecho, cada módulo de nivel más bajo debe ejecutar una tarea bien definida. Estos

módulos se denominan altamente cohesivos.

P:252

656 Fundamentos de programación

Leer las notas

de la lista Ordenar la lista Obtener el elemento

central de la lista

Encontrar la media

Pedir al usuario

una lista

Situar la nota

en la lista

Figura 18.2. Diagrama de bloques que muestra la jerarquía de módulos.

Cada módulo se puede dividir en subtareas. Por ejemplo, se puede refinar la tarea de leer las notas de una lista,

dividiéndolo en dos subtareas. Por ejemplo, se puede refinar la tarea de leer las notas de la lista en otras dos subtareas:

pedir al usuario una nota y situar la nota en la lista.

El diseño descendente (también llamado refinamiento sucesivo) comienza en el nivel superior (problema original)

y lo divide en subproblemas. Para cada subproblema, se identifica un subsistema con la responsabilidad de resolver ese subproblema. Se utiliza un diagrama o carta de estructura para indicar las relaciones entre los subproblemas y los subsitemas.

18.1.3. Abstracción procedimental

Cada algoritmo que resuelve el diseño de un módulo equivale a una caja negra que ejecuta una tarea determinada. Cada caja negra especifica lo que hace pero no cómo lo hace, y de igual modo cada caja negra conoce cuántas cajas

negras existen y lo que hacen.

Normalmente, estas cajas negras se implementan como subprogramas. Una abstracción procedimental separa

el propósito de un subprograma de su implementación. Una vez que un subprograma se haya escrito o codificado, se

puede usar sin necesidad de conocer su cuerpo y basta con su nombre y una descripción de sus parámetros.

En resumen, la abstracción procedimental es la filosofía mediante la cual el desarrollo de un procedimiento (en

Pascal) o una función (en C++, Java,...) están separados los temas relativos a lo qué se consigue con la función o

procedimiento de los detalles de cómo se consigue. En otras palabras, se puede especificar lo que se espera haga una

función y a continuación usar esa función en el diseño de la solución de un problema antes de conocer cómo implementar la función.

La modularidad y abstracción procedimental son complementarios. La modularidad implica romper una solución

en módulos; la abstracción procedimental implica la especificación de cada módulo antes de su implementación. El

módulo implica que se puede cambiar su algoritmo concreto sin afectar el resto de la solución.

La abstracción procedimental es esencial en proyectos complejos, de modo que se puedan utilizar subprogramas

escritos por otras personas sin necesidad de tener que conocer sus algoritmos.

18.1.4. Abstracción de datos

La abstracción procedimental significa centrarse en lo que hace un módulo en vez de en los detalles de cómo se implementan los detalles de sus algoritmos. De modo similar, la abstracción de datos se centra en las operaciones que

se ejecutan sobre los datos en vez de cómo se implementarán las operaciones.

Como ya se ha comentado antes, un tipo abstracto de datos (TAD) es una colección de datos y un conjunto de

operaciones sobre esos datos. Tales operaciones pueden añadir nuevos datos, o quitar datos de la colección, o buscar

algún dato. Los otros módulos de la solución conocerán qué operaciones puede realizar un TAD. Sin embargo, no

conoce cómo se almacenan los datos ni cómo se realizan esas operaciones.

P:253

Resolución de problemas y desarrollo de software: Metodología de la programación 657

Cada TAD se puede implementar utilizando estructuras de datos. Una estructura de datos es una construcción

que se puede definir dentro de un lenguaje de programación para almacenar colecciones de datos. En la resolución

de un problema, los tipos abstractos de datos soportan algoritmos y los algoritmos son parte de lo que constituye un

TAD. Para diseñar una solución, se debe desarrollar los algoritmos y los TAD al unísono.

En el caso de la programación orientada a objetos, el concepto de abstracción de datos, consiste en especificar

los objetos datos de un problema y las operaciones que se ejecutan sobre esos datos, sin preocuparse de cómo se

representan y almacenan los objetos en el memoria. Esta visión se conoce como vista lógica del objeto dato, como

opuesta a la vista física, su representación interna en memoria. Una vez que se entiende la vista lógica, se puede

utilizar el objeto dato y sus operaciones (funciones miembro en C++) en sus programas; sin embargo, tendría que,

eventualmente, implementar el objeto dato y su operador (función miembro) antes que se pueda ejecutar a cualquier

programa que lo utilice.

Un caso típico del uso de abstracción de datos es el caso del tipo de dato cadena (string) en C++ que representa una secuencia de caracteres pero no necesita para su uso conocer cómo se almacena en memoria la secuencia

de caracteres que constituyen la cadena. La cadena string en C++ es una abstracción de una secuencia de caracteres. Se pueden utilizar los objetos string y sus funciones miembros (length, copy, etc.) sin conocer los detalles

de su implementación.

18.1.5. Ocultación de la información

La abstracción identifica los aspectos esenciales de módulos y estructura de datos que se pueden tratar como cajas

negras. La abstracción es responsable de sus vistas externas o públicas, pero también ayuda a identificar los detalles

que debe ocultar de la vista pública (vista privada). El principio de ocultación de la información no sólo oculta los

detalles dentro de la caja negra, sino que asegura que ninguna otra caja negra pueda acceder a estos detalles ocultos.

Por consiguiente, se deben ocultar ciertos detalles dentro de sus módulos y TAD y hacerlos inaccesibles a otros módulos y TAD.

Un usuario de un módulo no se preocupa sobre los detalles de su implementación y, al contrario, un desarrollador

de un módulo o TAD no se preocupa sobre sus usos.

En el caso de la orientación a objetos, las clases pueden acceder a los datos de los objetos sólo a través de sus

funciones miembro. El proceso de “ocultar” los detalles de la implementación de una clase se denomina “ocultación

de la información”.

18.1.6. Programación orientada a objetos

Los conceptos de modularidad, abstracción procedimental, abstracción de datos y ocultación de la información conducen a la programación orientada a objetos, basada en el módulo o tipo de dato objeto.

Las prioridades fundamentales de la orientación a objetos son: encapsulamientos, herencia y polimorfismo. El

encapsulamiento es la combinación de datos y operaciones que se pueden ejecutar sobre esos datos en un objeto.

En C++/Java el encapsulamiento en un objeto se codifica mediante una clase.

Herencia es la propiedad que permite a un objeto transmitir sus propiedades a otros objetos denominados descendientes; la herencia permite la reutilización de objetos que se hayan definido con anterioridad. El polimorfismo

es la propiedad que permite decidir en tiempo de ejecución la función a ejecutar, al contrario que sucede cuando no

existe polimorfismo, en el que la función a ejecutar se decide previamente y sin capacidad de modificación, en tiempo de compilación.

18.1.7. Diseño orientado a objetos

El diseño descendente es un diseño orientado a procesos que se centra en las acciones que son necesarias en lugar

de en las estructuras de datos. En contraste, el diseño orientado a objetos (DOO) se centra en los elementos dato que

son necesarios y en las operaciones que se realizan sobre esos elementos dato. En el DOO se identifican primero los

objetos que existen en su problema y a continuación se identifica cómo interactuan para encontrar la solución. Las

características comunes de una colección de objetos similares definen una clase y las interacciones se identifican con

mensajes que un objeto envía a otro objeto. Para que un objeto reciba un mensaje, la clase a que pertenece debe proporcionar un operador (normalmente una función) para procesar ese mensaje. Normalmente los objetos se deducen

P:254

658 Fundamentos de programación

de la descripción del problema buscando los posibles nombres que puedan existir, mientras que las funciones se encuentran entre los verbos existentes.

El DOO incorpora lógicamente elementos del diseño descendente e incluso del ascendente y como herramienta

de diseño utiliza UML como Lenguaje Unificado de Modelado. Los diagramas UML son un medio estándar de describir las clases y documentar las relaciones entre ellas con el objeto de implementar los programas que permitirán

resolver los problemas planteados.

18.2. EL CICLO DE VIDA DEL SOFTWARE

Existen dos niveles en la construcción de programas: aquellos relativos a pequeños programas (los que normalmente

realizan programadores individuales) y aquellos que se refieren a sistemas de desarrollo de programas grandes (proyectos de software) y que, generalmente, requieren un equipo de programadores en lugar de personas individuales.

El primer nivel se denomina programación a pequeña escala; el segundo nivel se denomina programación a gran

escala.

La programación en pequeña escala se preocupa de los conceptos que ayudan a crear pequeños programas —aquellos que varían en longitud desde unas pocas líneas a unas pocas páginas—. En estos programas se suele requerir

claridad y precisión mental y técnica. En realidad, el interés mayor desde el punto de vista del futuro programador

profesional está en los programas de gran escala que requiere de unos principios sólidos y firmes de lo que se conoce como ingeniería de software y que constituye un conjunto de técnicas para facilitar el desarrollo de programas de

computadora. Estos programas o mejor proyectos de software están realizados por equipos de personas dirigidos por

un director de proyectos (analista o ingeniero de software) y los programas pueden tener más de 100.000 líneas de

código.

La técnica utilizada por los desarrolladores profesionales de software es comprender lo mejor posible el problema

que se está tratando de resolver y crear una solución de software apropiada y eficiente que se denomina proceso de

desarrollo de software.

Un producto de software se desarrolla en varias etapas desde su concepción inicial al producto terminado para su

uso regular. Esta secuencia de etapas se conoce como ciclo de vida y en este caso particular se denomina ciclo de

vida del software.

Los productos de software pueden requerir años de desarrollo y también los esfuerzos de muchas personas, analistas, programadores, usuarios, etc. Un producto de software con éxito se utilizará durante muchos años después de

su primera versión. Durante su período de uso, versiones nuevas o actualizadas se lanzarán de modo que irán conteniendo la fijación y corrección de los diferentes errores que hayan surgido. Por esta razón, es importante diseñar y

documentar el software de modo que se pueda comprender y mantener fácilmente después de su versión inicial. Esto

es especialmente importante ya que las personas que mantienen el software pueden no haber estado implicados en el

diseño original.

18.2.1. El ciclo de vida del software tradicional (modelo en cascada)

Existen muchos modelos de proceso de software que se han propuesto durante las últimas décadas para desarrollar

el ciclo de vida del software. De igual modo existen muchos métodos diferentes de organizar las actividades que

transforman el software de una etapa a otra. La versión más simple y más utilizada es el modelo en cascada, en el

cual se ejecutan las actividades de modo secuencial, de modo que el resultado de cada una de ellas es la entrada de

la siguiente. Gráficamente se representa con una flecha que apunta de una etapa a la siguiente.

La Figura 18.3 muestra el modelo del ciclo de vida del software en cascada que se compone de cinco fases o

etapas claves; Análisis, Diseño, Implementación (Construcción), Pruebas, Instalación (Despliegue) y Mantenimiento.

Las actividades a realizar en cada una de las etapas en el modelo en cascada son las siguientes:

1. Análisis. Esta etapa se descompone, a su vez, en Análisis de requisitos y Análisis. En el análisis de requisitos

se definen y determinan los requisitos del sistema (análisis y especificaciones). Una vez que se han especificado los requisitos del sistema, comienza la subetapa de análisis, cuyo objetivo es determinar cuidadosamente los requisitos de entrada y salida del sistema y su interacción con el usuario.

2. Diseño. Una vez se conocen los requisitos del sistema, el proceso de diseño determina cómo construir un

sistema que cumpla estos requisitos. Se define la arquitectura del sistema: componentes, interfaces, relaciones

P:255

Resolución de problemas y desarrollo de software: Metodología de la programación 659

y comportamiento. Se definen las funciones y los datos (o clases, en programación orientada a objetos) así

como los algoritmos de las funciones.

3. Implementación (construcción). El diseño terminado se traduce en el código del programa que implementará el software a desarrollar. La construcción puede utilizar diferentes lenguajes de programación (C, C++,

Java, C#,...) y sistemas de gestión de bases de datos, para diferentes partes del sistema. Se codifican las funciones y clases individuales (en el caso de programación orientada a objetos) en el lenguaje de programación

elegido.

4. Pruebas (testing). El sistema se comprueba para asegurar que cumple los requisitos del usuario y que funciona adecuadamente. Las funciones y las clases se prueban aisladas y como unidades.

5. Instalación (Despliegue). Una vez que el sistema (el producto de software terminado) se ha probado satisfactoriamente, se entrega al cliente y se instala para su utilización posterior.

6. Mantenimiento. Una vez entregado el producto se deben detectar posibles fallos y eliminarlos (mantenimiento correctivo). Ciertos aspectos del comportamiento del sistema pueden no haberse implementado en su totalidad (por ejemplo por costes o restricciones de tiempo) y se han de corregir durante la fase de mantenimiento (mantenimiento correctivo). El entorno operativo puede cambiar durante su vida útil produciendo cambios

en los requisitos que han de adaptarse y acomodarse en su evolución y desarrollo (mantenimiento adaptativo).

Existen muchas variaciones al modelo en cascada [PRESSMAN 04], [SOMMERVILLE 98] difiriendo, esencialmente, en el número y nombre de las etapas, aunque en esencia el resultado final siempre será el mismo.

PRESSMAN SOMMERVILLE

Requisitos Ingeniería de sistemas

Planeación Análisis de requisitos

Modelado Diseño

Construcción Construcción

Despliegue Instalación

Mantenimiento

En nuestra tercera edición considerábamos para el ciclo de vida, las etapas siguientes: Análisis, Diseño, Implementación, Depuración (pruebas) y Mantenimiento.

En este modelo, el flujo fundamental de actividades supone que cada etapa deber ser terminada antes de que comience la siguiente, o dicho de otro modo, la salida de una etapa es la entrada de la siguiente. Sin embargo, raramente sucede esto en la práctica. Por ejemplo, los diseñadores del sistema pueden identificar requisitos incompletos

Instalación

(Despliegue)

Mantenimiento

Análisis

Diseño

Implementación

Pruebas

Figura 18.3. Ciclo de vida del software en cascada.

P:256

660 Fundamentos de programación

o inconsistentes durante el proceso de diseño, o los programadores (más cercana esta etapa a los objetivos de nuestra

obra) pueden encontrar durante la fase de implementación que hay áreas del diseño que están incompletas o son inconsistentes. A veces, hasta que el producto no está terminado, el cliente no ve que los requisitos especificados no

se han cumplido hasta no observar los fallos o quejas de los usuarios.

El ciclo de vida tradicional (en cascada) ha sido utilizado durante muchos años y se sigue utilizando, pero es

objeto de muchas críticas ya que presenta algunos problemas, a veces difícil de resolver, tales como los siguientes:

• Los proyectos reales, raramente, siguen un ciclo de vida secuencial. Fases del proyecto se solapan y algunas

actividades tienen que ser repetidas.

• Las iteraciones son casi inevitables, ya que las insuficiencias o deficiencias en el análisis de requisitos pueden

hacerse evidentes durante el diseño, construcción o pruebas.

• Gran parte del tiempo transcurre entre la fase de especificaciones de requisitos y la instalación final. Los requisitos, inevitablemente, cambian en el transcurso del tiempo y eso implica que las operaciones reales se vean

afectadas y no sea fácil las modificaciones necesarias.

• El modelo tradicional no da respuesta fácil a los cambios en los requisitos del cliente o en las tecnologías

durante el proyecto. Una vez que se han tomado las decisiones arquitectónicas, son difíciles de cambiar. Una

innovación tecnológica suele ser difícil de incorporar ya que casi siempre requerirá rehacer muchos de los trabajos de análisis y diseño.

Por estas razones el ciclo de vida del software es un proceso iterativo, de modo que se modificarán las sucesivas

etapas en función de la modificación de las especificaciones de los requisitos producidos en la fase de diseño o implementación, o una vez que el sistema se ha implementado y probado pueden aparecer errores que es necesario

corregir y depurar, y que requieren la repetición de etapas anteriores. Por estas razones es muy usual el uso de bucles

de realimentación entre etapas, aunque a veces las iteraciones resultantes son muy costosas. Muchas veces, una deficiencia importante en el análisis de requisitos se descubre durante la construcción, de modo que la redefinición de

requisitos puede conducir a nuevas implementaciones que no siempre son fáciles de construir. Por estas razones, a

veces, se suele asignar a las diferentes etapas, equipos especializados; es decir, el análisis lo hace un equipo especializado, otros el diseño y otros las pruebas.

Diversas alternativas se han propuesto para mejorar el modelo en cascada. Prototipado, modelo iterativo e incremental, proceso unificado, métodos ágiles, etc. El método más usual para el desarrollo de software es adoptar un ciclo

de vida iterativo, desarrollando un producto de software en etapas o ciclos. Cada etapa es una mini-versión del modelo en cascada, con énfasis en las diferentes actividades del ciclo de vida. Al final de cada ciclo hay una revisión con

los usuarios para obtener realimentación que se tendría en cuenta en la siguiente etapa. Este último modelo se conoce

como desarrollo iterativo e incremental. Pressman considera diferentes modelos de procesos incrementales: modelo

DRA (Desarrollo rápido de aplicaciones), prototipado (construcción de prototipos), modelo en espiral. Considera

también modelos especializados de procesos, basados en componentes, métodos formales y orientados a aspectos.

Un modelo que ha tenido bastante aceptación en los últimos años es el Proceso Unificado basado en los trabajos

de Jacobson, Rumbaugh y Booch, dirigido esencialmente a modelos orientados a objetos y soportados en el lenguaje UML.

18.2.2. El proceso unificado

El Proceso Unificado de Desarrollo de Software (The Unified Software Development Process, USDP), popularmente conocido como Proceso Unificado (PU), presenta el énfasis real en los ciclos de vida iterativo e incremental. El

PU tiene sus antecedentes en los métodos de Jacobson [JACOBSON 92], Booch [BOOCH 94] y Rumbaugh [RUMBAUGH 01], y su especificación formal se hizo en [Jacobson 98]. El Proceso Unificado incorporó UML y contiene

muy buenas reglas y consejos para el desarrollo del software.

Los ciclos se denominan fases e iteraciones que se muestran en el eje horizontal de un diagrama de tiempos, y

las actividades, denominadas flujos de trabajo (workflow) se muestran en el eje vertical del mismo diagrama de tiempos. Las cuatro fases son: incepción, elaboración, construcción y transición.

• Incepción. Determinación del ámbito y propósito del proyecto.

• Elaboración. Se centra en la captura de requisitos y en la determinación de la estructura del sistema.

• Construcción. Su objetivo principal es construir el sistema de software.

• Transición. Instalación y despliegue del producto.

P:257

Resolución de problemas y desarrollo de software: Metodología de la programación 661

El tiempo en el eje horizontal se desplaza desde la iteración a la iteración n, y cada iteración es una mini-cascada. Las cinco/seis actividades son las mismas que en el modelo en cascada más simple. Las áreas sombreadas bajo

las curvas a continuación de cada actividad están pensadas para mostrar la cantidad relativa de esfuerzo gastado en

cada actividad durante cada iteración. Por ejemplo, durante la fase de incepción (iteraciones 1 y 2) el esfuerzo mayor

se gasta en especificar los requisitos y un ligero esfuerzo en el análisis. De hecho, durante la iteración 1 la única

actividad que se realiza es la especificación de requisitos. Durante la iteración 2 se gasta esfuerzo en el análisis, requisitos y un pequeñísimo esfuerzo en el diseño y la implementación.

A medida que los desarrolladores de software se mueven en la fase de elaboración, continúa el trabajo en las

actividades de análisis y requisitos, y ya comienza a emplearse tiempo en las fases de diseño e implementación. En

la fase de construcción, la especificación de requisitos y el análisis prácticamente han terminado y la mayoría del

esfuerzo se gasta en las actividades de diseño e implementación. El diagrama muestra que las pruebas se realizan,

con la excepción de la fase de incepción, en todas las fases restantes, aunque el mayor tiempo se gasta en las fases

de construcción y de transición.

18.2.3. Cliente, desarrollador y usuario

Antes de continuar vamos a dar una serie de definiciones implicadas en el desarrollo de software [Schach 02]. El

cliente es el individuo u organización que desea se desarrolle un producto. Los desarrolladores son los miembros

de la organización responsable de construir el producto. Los desarrolladores pueden ser los responsables de todos

los aspectos del proceso, desde la fase de requisitos en adelante o pueden ser responsables sólo de la implementación de un producto ya diseñado. El término desarrollo de software abarca (cubre) todos los aspectos de la producción del software antes de que el producto entre en la fase de mantenimiento. Cualquier tarea que comprenda una

etapa hacia la construcción de una pieza de software, incluyendo especificación, planificación, diseño, pruebas o

documentación constituyen el desarrollo del software. Y después que ha sido desarrollado, el software está mantenido.

Tanto los clientes como los desarrolladores pueden ser parte de la misma organización. Por ejemplo, el cliente

puede ser el director de contabilidad de una compañía de seguros y los desarrolladores, un equipo dirigido por el

director de sistemas de información de la citada compañía. Este desarrollo se denomina software interno. Si los clientes y los desarrolladores son totalmente independientes de la organización, entonces se denomina software por contrato.

Comunicación Modelado

Construcción

Despliegue

Elaboración

Inicio

Construcción

Transición

Producción

Lanzamiento

Planificación

Incremento del software

Figura 18.4. Proceso Unificado de Desarrollo de Software.

(FUENTE: Adaptado del original: The Unified Software Development Process, Jacobson, Booch

y Rumbaugh, Addison-Wesley, 1999.)

P:258

662 Fundamentos de programación

La tercera parte implicada en la producción de software es el usuario. El usuario es la persona o personas a las

que el cliente permite utilizar el producto sofware bien gratuitamente o bien mediante el pago de una licencia. En

la compañía de seguros, los usuarios pueden ser los agentes de seguros, los cuales utilizan el software para seguir la

normativa de la compañía en lo referente a las pólizas de seguros y seguros de vida. En algunos casos el cliente y

el usuario pueden ser la misma persona.

En el extremo opuesto al software a medida escrito para un cliente, se pueden vender copias múltiples de software,

tales como procesadores de palabras u hojas de cálculo, a precios muchos más bajos, debido esencialmente al gran

número de copias que se fabrican y venden al estilo de cualquier producto comercial. Es decir, los fabricantes de este

tipo de software (tales como Microsoft, Oracle o IBM) recuperan el alto coste de desarrollo por la venta de grandes

volúmenes de copias.

18.3. FASE DE ANÁLISIS: REQUISITOS Y ESPECIFICACIONES

La primera etapa en la producción de un sistema de software es decidir exactamente qué se supone ha de hacer el

sistema; esta etapa se conoce también como análisis de requisitos o especificaciones y por esta circunstancia muchos

tratadistas suelen subdividir la etapa en otras dos:

• Análisis y definición del problema (requisitos).

• Especificación de requisitos (especificaciones).

La parte más difícil en la tarea de crear un sistema de software es definir cuál es el problema y a continuación

especificar lo que se necesita para resolverlo. Normalmente la definición del problema comienza analizando los requisitos del usuario, pero estos requisitos, con frecuencia, suelen ser imprecisos y difíciles de describir. Se deben

especificar todos los aspectos del problema, pero con frecuencia las personas que describen el problema no son programadores y eso hace imprecisa la definición. La fase de especificación requiere normalmente la comunicación

entre los programadores y los futuros usuarios del sistema e iterar la especificación hasta que tanto el especificador

como los usuarios estén satisfechos de las especificaciones y hayan resuelto el problema normalmente.

En la etapa de especificaciones puede ser muy útil para mejorar la comunicación entre las diferentes partes implicadas construir un prototipo o modelo sencillo del sistema final; es decir, escribir un programa prototipo que simule el comportamiento de las partes del producto software deseado. Por ejemplo, un programa sencillo —incluso

ineficiente— puede demostrar al usuario la interfaz propuesta por el analista. Es mejor descubrir cualquier dificultad

o cambiar su idea original ahora que después de que la programación se encuentre en estado avanzado o, incluso,

terminada. El modelado de datos es una herramienta muy importante en la etapa de definición del problema. Esta

herramienta es muy utilizada en el diseño y construcción de bases de datos.

Tenga presente que el usuario final, normalmente, no conoce exactamente lo que desea haga el sistema. Por consiguiente, el analista de software o programador, en su caso, debe interactuar con el usuario para encontrar lo que el

usuario deseará haga el sistema. En esta etapa se debe responder a preguntas tales como:

— ¿Cuáles son los datos de entrada?

— ¿Qué datos son válidos y qué datos no son válidos?

— ¿Quién utilizará el sistema: especialistas cualificados o usuarios cualesquiera (sin formación)?

— ¿Qué interfaces de usuario se utilizarán?

— ¿Cuáles son los mensajes de error y de detección de errores deseables? ¿Cómo debe actuar el sistema

cuando el usuario cometa un error en la entrada?

— ¿Qué hipótesis son posibles?

— ¿Existen casos especiales?

— ¿Cuál es el formato de la salida?

— ¿Qué documentación es necesaria?

— ¿Qué mejoras se introducirán —probablemente— al programa en el futuro?

— ¿Cómo debe ser de rápido el sistema?

— ¿Cada cuánto tiempo ha de cambiarse el sistema después que se haya entregado?

El resultado final de la fase de análisis es un documento de especificación de los requisitos del software. Al contrario que la fase informal de requisitos, el documento de especificaciones (o especificaciones) describe explícitamente la funcionalidad del producto —es decir, con precisión, lo que se supone hace el producto— y lista cualquier

P:259

Resolución de problemas y desarrollo de software: Metodología de la programación 663

restricción que deba cumplir el producto. El documento de especificaciones incluye las entradas al producto y las

salidas requeridas. El documento de especificaciones del producto constituye un contrato. La fase de especificaciones

tiene dos salidas principales. La primera es el documento de especificaciones (especificaciones) y la segunda salida

es un plan de gestión del proyecto software.

El documento de especificaciones de requisitos para un nuevo producto de software debe ser generado al principio del proyecto y tanto los usuarios como los diseñadores deben revisar y aprobar dicho documento.

EJEMPLO 18.1

Sistema de nóminas de una empresa.

Las entradas han de incluir los rangos o escalas de nómina de cada empleado, los datos de períodos de tiempo

trabajados en la empresa, así como información de los archivos de personal, de modo que se puedan calcular correctamente los impuestos. La salida serán los cheques o transferencias bancarias así como informes de deducciones de

cuotas de la Seguridad Social. Además, el documento de especificaciones incluye las estipulaciones que debe cumplir

el producto para manipular correctamente un amplio rango de deducciones, tales como pagos de seguros médicos,

cuotas a sindicatos o contribuciones a planes de pensiones del empleado.

• El análisis del problema requiere asegurar que el problema está claramente definido y comprendido. Ello

requiere entender cuáles son las salidas requeridas y cuáles son las entradas necesarias.

• Descripción del problema previa y detalladamente.

• Prototipos de programas pueden clarificar el problema.

EJEMPLO 18.2

Definir el documento de especificación de requisitos de un programa que manipule el conjunto de los nombres de

los alumnos y números de teléfono de un determinado curso de una Facultad de Ingeniería, con el objeto de mantenerles informados mediante mensajes de texto SMS de cualquier noticia, calificación, calendario de exámenes, etc.

relativas al curso académico donde están matriculados.

El programa debe poder insertar nuevas entradas en el directorio o agenda, recuperar o modificar, una entrada del

directorio, además de la funcionalidad “enviar el mensaje de texto”. Un sistema para encontrar las especificaciones

que conduzcan a la elaboración del documento de especificaciones, podría ser responder a alguna de estas preguntas

u otras similares:

• ¿Existe una lista inicial de nombres y números de teléfonos, registrada de modo escrito o bien en un archivo

electrónico?

• En caso de no existir archivo electrónico, los datos escritos en papel se deben introducir todos a la vez y de

modo interactivo.

• En el caso de que exista una versión inicial de un archivo electrónico, ¿qué tipo de archivos es? Archivo binario, de texto...

• Si el archivo es de texto, ¿cuáles son las convenciones de formato? Los nombres tienen 30 caracteres, el número de teléfono tiene 9, 11..., dígitos (incluye el código del país, de provincia o departamento...), es celular (móvil)

o fijo.

• ¿Cuál es el formato de los nombres: primer apellido, segundo apellido, nombre, o al contrario nombre, apellido

primero y apellido segundo?

• ¿Es posible que un alumno tenga asociado más de un número de teléfono? En caso afirmativo, cuando se introduzcan o recuperen números, ¿cuál número se recupera primero, cuál segundo...?

• Cuándo se recupera un nombre de un alumno, ¿se debe visualizar el nombre, el número o números de teléfono,

o sólo el número de teléfono...?

• ¿Qué hacer si al introducir un nuevo nombre de un alumno, este ya existe o resulta que existe otro alumno con

el mismo nombre?

P:260

664 Fundamentos de programación

• ¿Cómo se debe hacer para modificar el número de teléfono de un alumno, o un nuevo número si se admiten

más de uno en la lista?

• ¿...?

Es decir, en un problema, teóricamente sencillo, se han de plantear numerosas cuestiones en la descripción del

problema inicial que conduzcan al documento de especificación de requisitos. Muchas de estas preguntas son relativas a detalles de datos de entrada o de salida, errores potenciales, formatos de entrada y de salida, etc. Naturalmente, mientras más claro sea el documento de especificación de requisitos, más fácil será analizar y diseñar el

problema.

Una vez que los requisitos del sistema se han especificado, comienza la etapa de análisis. Antes de comenzar el

diseño de una posible solución del problema, se debe estar seguro de que el problema está totalmente comprendido.

Si la especificación de requisitos se ha realizado cuidadosamente, el análisis será fácil, si quedan cuestiones sin resolver, deberán resolverse antes de entrar en la nueva etapa de diseño. En la empresa y en la industria, el analista de

sistema junto con los programadores y los usuarios deben considerar si existe un paquete comercial de software que

cumpla con los requisitos (es una alternativa al desarrollo del software a medida). Deben determinar el impacto del

nuevo producto de software en los sistemas informáticos de la organización y en este caso cuál es el nuevo hardware

y software que se necesita para ejecutar el nuevo sistema. En caso de que la solución sea diseñar un nuevo producto

de software, se debe determinar la fiabilidad que se debe alcanzar, estimación de los costes y beneficios, así como

una previsión de tiempo de desarrollo. También se debe definir cuál es el mejor método de diseño a realizar. Se debe

diseñar e implementar cada programa, pero en el análisis, un objetivo es determinar con fiabilidad los requisitos de

entrada/salida del sistema y sus interacciones con el usuario. Debe también pensar en romper el sistema en pequeños

componentes, que sean fáciles de diseñar y codificar independientemente. Para ello necesitará identificar los módulos

o componentes que constituyen el sistema y especificar las interacciones entre ellos. Una vez que se ha terminado

este proceso se debe comenzar con la fase de diseño.

La siguiente etapa es el diseño o codificación del producto.

18.4. DISEÑO

La especificación de un sistema indica lo que el sistema debe hacer. La etapa de diseño del sistema indica cómo ha

de hacerse. Para un sistema pequeño, la etapa de diseño puede ser tan sencilla como escribir un algoritmo en pseudocódigo. Para un sistema grande, esta etapa incluye también la fase de diseño de algoritmos, pero incluye el diseño

e interacción de un número de algoritmos diferentes, con frecuencia sólo bosquejados, así como una estrategia para

cumplir todos los detalles y producir el código correspondiente.

Arrancando con las especificaciones, el equipo de diseño determina la estructura interna del producto. Los diseñadores descomponen el producto en módulos, piezas independientes de código con interfaces bien definidas al resto del producto. (Un objeto es un tipo específico de módulo). La interfaz de cada módulo, es decir, los argumentos

que se pasan al módulo y los argumentos que se devuelven del módulo se deben especificar en detalle.

Una vez que el equipo ha completado la descomposición en módulos (diseño arquitectónico) se realiza el diseño

detallado. Para cada módulo se seleccionan los algoritmos y las estructuras de datos elegidas.

Es preciso determinar si se pueden utilizar programas o subprogramas que ya existen o es preciso construirlos

total mente. El proyecto se ha de dividir en módulos utilizando los principios de diseño descendente. A continuación,

se debe indicar la interacción entre módulos; un diagrama de estructuras proporciona un esquema claro de estas relaciones1

.

En este punto es importante especificar claramente no sólo el propósito de cada módulo, sino también el flujo

de datos entre módulos. Por ejemplo, se debe responder a las siguientes preguntas: ¿Qué datos están disponibles

al módulo antes de su ejecución? ¿Qué supone el módulo? ¿Qué hacen los datos después de que se ejecuta el módulo? Por consiguiente, se deben especificar en detalle las hipótesis, entrada y salida para cada módulo. Un medio

para realizar estas especificaciones es escribir una precondición, que es una descripción de las condiciones que

deben cumplirse al principio del módulo y una postcondición, que es una descripción de las condiciones al final

1

Para ampliar sobre este tema de diagramas de estructuras, puede consultar estas obras nuestras: Fundamentos de programación, 2.ª edición,

McGraw-Hill, 1992; Problemas de metodología de la programación, McGraw-Hill, 1992, o bien la obra Programación en C. Joyanes y Zahonero, McGraw-Hill, 2001.

P:261

Resolución de problemas y desarrollo de software: Metodología de la programación 665

de un módulo. Por ejemplo, se puede describir un procedimiento que ordena una lista (un array) de la forma siguiente:

procedimiento ordenar (E/S arr:A; E entero: n)

{Ordena una lista en orden ascendente

precondición: A es un array de n enteros, 1<= n <= Max.

postcondición: A[1] <= A[2] <...<= A[n], n es inalterable}

Por último, se puede utilizar pseudocódigo2

para especificar los detalles del algoritmo. Es importante que se

emplee bastante tiempo en la fase de diseño de sus programas. El resultado final de diseño descendente es una solución que sea fácil de traducir en estructuras de control y estructuras de datos de un lenguaje de programación específico, por ejemplo, Pascal o C.

El diseño es una actividad de sólo ingeniería. Su entrada principal es la especificación (o el documento de requisitos) que se traducirá en un documento de diseño, escrito por los programadores o ingenieros de software.

Desde el punto de vista estricto de programación, en esta etapa se construye (diseña) el algoritmo que se utilizará para resolver el problema. La solución normalmente se obtiene por una serie de refinamientos que comienza con el algoritmo inicial encontrado en la fase de análisis hasta que se obtiene un algoritmo completo y aceptable.

El gasto de tiempo en la fase de diseño será ahorro de tiempo cuando escriba y depure su programa.

Una vez que se ha entendido el problema en profundidad y se ha elegido el enfoque global a aplicar, el ingeniero de software (el programador en su defecto) debe plantearse cuál enfoque de diseño a aplicar: diseño descendente o diseño orientado a objetos.

En el diseño descendente, un sistema se rompe en un conjunto de subsistemas más pequeños, cada uno de

estos subsistemas se rompe en componentes más pequeños y así sucesivamente, hasta encontrar unos pequeños y sencillos de codificar fácilmente.

En el diseño orientado a objetos, el desarrollador identifica un conjunto de objetos y especifica su inte r acción

(véase Ejemplo 18.2.).

EJEMPLO 18.3

Escribir un programa que construya un directorio telefónico de alumnos con sus nombres y números de teléfonos de

modo que se puedan enviar a dichos alumnos mensajes de texto SMS con noticias, calificaciones, fechas de exámenes,

anuncios de conferencias, etc.

Diseño descendente

El problema de construir un directorio telefónico se descompone a su vez en, por ejemplo, cuatro subproblemas:

Lectura del directorio inicial, Introducir una nueva entrada, Editar una entrada y

Recuperar y visualizar una entrada. Este primer nivel de descomposición, se descompone en un nivel

más inferior mediante un refinamiento, nivel 2; por ejemplo los subproblemas: Lectura del directorio inicial, Introducir una nueva entrada, Editar una entrada y Recuperar y visualizar una

entrada.

2

Para consultar el tema del pseudocódigo, véase las obras: Fundamentos de programación. Algoritmos y estructuras de datos, 2.ª edición,

McGraw-Hill, 1996, de Luis Joyanes, y Fundamentos de programación. Libro de problemas, McGraw-Hill, 1996, de Luis Joyanes, Luis Rodríguez

y Matilde Fernández.

P:262

666 Fundamentos de programación

Diseño orientado a objetos

En la definición del problema se detectan los nombres: Directorio, Entrada y Archivo, que constituyen las

clases. La clase Directorio contiene funciones que leen o escriben información de la clase Archivo. Las clases

Archivo y Directorio contienen entradas con los campos Nombre y NúmerodeTeléfono. Por consiguiente el

diagrama de clases para resolver el problema es muy simple.

El actor Usuario envía una orden a la clase Directorio, con el objeto de activar el programa. El Directorio

contiene un conjunto de Entradas. Archivo, a su vez, contiene un conjunto de entradas, y por último el Directorio lee datos de un archivo y escribe datos en el mismo archivo.

18.5. IMPLEMENTACIÓN (CODIFICACIÓN)

La etapa de implementación (codificación) traduce los algoritmos del diseño en un programa escrito en un lenguaje

de programación. Los algoritmos y las estructuras de datos realizadas en pseudocódigo han de traducirse a un lenguaje que entiende la computadora.

La codificación ha de realizarse en un lenguaje de programación. Los lenguajes clásicos más populares son PASCAL, FORTRAN, COBOL y C; los lenguajes orientados a objetos más usuales son C++, Java, Visual BASIC.NET,

Smaltalk, y recientemente C#, etc.

Si un problema se divide en subproblemas, los algoritmos que resuelven cada subproblema (tarea o módulo) deben ser codificados, depurados y probados independientemente.

Es relativamente fácil encontrar un error en un procedimiento pequeño. Es casi imposible encontrar todos los

errores de un programa grande, que se codificó y comprobó como una sola unidad en lugar de como una colección

de módulos (procedimientos) bien definidos.

Las reglas del sangrado (indentación) y buenos comentarios facilitan la escritura del código. El pseudocódigo es

una herramienta excelente que facilita notablemente la codificación.

Codificar la solución consiste en escribir el programa e implementar la solución y se manifiesta en la traducción

del algoritmo en un programa comprensible por la computadora.

18.6. PRUEBAS E INTEGRACIÓN

La etapa de pruebas requiere, como su nombre sugiere, la prueba o verificación del programa de computadora terminado al objeto de asegurar lo que hace; de hecho proporciona una solución al problema. Cualquier error que se

encuentre durante esta prueba o test se debe corregir. Cuando los diferentes componentes de un programa se han

implementado y comprobado individualmente, el sistema completo se ensambla y se integra.

La etapa de pruebas sirve para mostrar que un programa es correcto. Las pruebas nunca son fáciles. Edgar Dirjkstra ha escrito que mientras que las pruebas realmente muestran la presencia de errores, nunca pueden mostrar su

ausencia. Una prueba con “éxito” en la ejecución significa sólo que no se han descubierto errores en esas circunstancias específicas, pero no se dice nada de otras circunstancias. En teoría el único modo que una prueba puede

mostrar que un programa es correcto si todos los casos posibles se han intentado y comprobado (es lo que se conoce como prueba exhaustiva); es una situación técnicamente imposible incluso para los programas más sencillos.

Supongamos, por ejemplo, que se ha escrito un programa que calcule la nota media de un examen. Una prueba

exhaustiva requerirá todas las combinaciones posibles de notas y tamaños de clases; puede llevar muchos años

completar la prueba.

La fase de pruebas es una parte esencial de un proyecto de programación. Durante la fase de pruebas se necesita

eliminar tantos errores lógicos como pueda. En primer lugar, se debe probar el programa con datos de entrada válidos

que conducen a una solución conocida. Si ciertos datos deben estar dentro de un rango, se deben incluir los valores

en los extremos finales del rango. Por ejemplo, si el valor de entrada de n cae en el rango de 1 a 10, se ha de asegurar incluir casos de prueba en los que n esté entre 1 y 10. También se deben incluir datos no válidos para comprobar

la capacidad de detección de errores del programa. Se han de probar también algunos datos aleatorios y por último

intentar algunos datos reales.

P:263

Resolución de problemas y desarrollo de software: Metodología de la programación 667

Cuando los diferentes componentes de un programa se han implementado y comprobado individualmente, el

sistema completo se ensambla y se integra.

18.6.1. Verificación

La etapa de pruebas ha de comenzar tan pronto como sea posible en la fase de diseño y continuará a lo largo de la

implementación del sistema. Incluso aunque las pruebas son herramientas extremadamente válidas para proporcionar

la evidencia de que un programa es correcto y cumple sus especificaciones, es difícil conocer si las pruebas realizadas son suficientes. Por ejemplo, ¿cómo se puede conocer que son suficientes los diferentes conjuntos de datos de

prueba o que se han ejecutado todos los caminos posibles a través del programa?

Por esas razones se ha desarrollado un segundo método para demostrar la corrección o exactitud de un programa.

Este método, denominado verificación formal, implica la construcción de pruebas matemáticas que ayudan a determinar si los programas hacen lo que se supone han de hacer. La verificación formal implica la aplicación de reglas

formales para mostrar que un programa cumple su especificación: la verificación. La verificación formal funciona

bien en programas pequeños, pero es compleja cuando se utiliza en programas grandes. La teoría de la verificación

requiere conocimientos matemáticos avanzados y por otra parte se sale fuera de los objetivos de este libro; por esta

razón sólo hemos constatado la importancia de esta etapa.

La prueba de que un algoritmo es correcto es como probar un teorema matemático. Por ejemplo, probar que un

módulo es exacto (correcto) comienza con las precondiciones (axiomas e hipótesis en matemáticas) y muestra que

las etapas del algoritmo conducen a las postcondiciones. La verificación trata de probar con medios matemáticos que

los algoritmos son correctos.

Si se descubre un error durante el proceso de verificación, se debe corregir su algoritmo y posiblemente se han

de modificar las especificaciones del problema. Un método es utilizar invariantes (una condición que siempre es

verdadera en un punto específico de un algoritmo) lo que probablemente hará que su algoritmo contenga pocos

errores antes de que comience la codificación. Como resultado, se gastará menos tiempo en la depuración de su

programa.

18.6.2. Técnicas de pruebas

Una vez que se ha generado el código fuente de un programa es necesario probar el software para detectar o descubrir y corregir la mayor cantidad de errores posibles antes de entregarlo al cliente. El software se ha de verificar, su

exactitud (corrección), mediante técnicas de pruebas (testing). Desgraciadamente las pruebas son una ciencia inexacta; no se puede declarar que una pieza o módulo de software es correcta vía pruebas a menos que éstas sean exhaustivas y en todos los escenarios posibles; pero, incluso en los programas sencillos hay, con frecuencia, cientos de

miles de caminos que pueden recorrerse. Por consiguiente, probar todos los posibles caminos a través de un programa complejo es una tarea imposible [Brookshear 05].

Por otra parte, las pruebas representan un reto para los ingenieros de software que han desarrollado metodologías

de prueba. Las técnicas de prueba del software proporcionan directrices para pruebas de diseño: (1) comprobar la

lógica interna y las interfaces de todo componente del software; (2) comprobar los dominios de entrada y salida del

programa para descubrir errores en su función, comportamiento y desempeño.

Ya en el año 79, Glen Myers establecía una serie de reglas sobre las pruebas a realizar al software y que aún hoy

siguen vigentes [Pressman 05:124]:

• Las pruebas consisten en un proceso en el que se ejecuta un programa con la intención de encontrar un error

que no se ha descubierto.

• Un buen caso de prueba es aquel en el que hay una gran probabilidad de encontrar un error que aún no se ha

descubierto.

• Una prueba con éxito es aquella que encuentra un error que no se descubría.

La observación ha demostrado que los errores en software tienden a ser repetidos. Esto es, la experiencia demuestra que un número pequeño de módulos dentro de un sistema de software grande tienden a ser más problemáticos

que el resto. Por consiguiente, la identificación de estos módulos y la prueba de ellos de modo más exhaustivo pueden encontrar errores del sistema que probar todos los módulos de modo uniforme. Esta característica se conoce como

principio de Pareto (en honor del economista italiano Wilfredo Pareto, 1848-1923, que estudió casos de teorías de

P:264

668 Fundamentos de programación

población en Italia con muestras similares relativas a Ciencias de la Salud) que demuestra que el 80 por ciento de los

errores descubiertos durante las pruebas en profundidad serán similares o rastreables en el 20 por ciento de todos

los programas. El problema, naturalmente, es aislar estos componentes sospechosos y después pro barlos.

Davis propone un conjunto de pruebas [Davis 95] además del ya citado principio de Pareto:

1. Todas las pruebas deben ser rastreables (seguidas) hasta los requisitos del cliente.

2. Las pruebas se deben planear mucho antes de que comience el proceso de prueba.

3. Las pruebas deben comenzar “en lo pequeño” y progresar hacia “lo grande”.

4. Las pruebas exhaustivas no son posibles.

Facilidad de las pruebas de software

El cliente prueba el programa cada vez que lo ejecuta; por consiguiente, se ha de ejecutar el programa antes de que

llegue al cliente y el objetivo concreto será encontrar y eliminar todos los errores. La localización de la mayor cantidad de errores requiere técnicas sistemáticas de pruebas. El objetivo de estas técnicas es encontrar errores y la calidad de estas técnicas se mide por su posibilidad de encontrar un error. En consecuencia, se trata de que las pruebas

sean fáciles con el objetivo de encontrar la mayor cantidad de errores con un mínimo esfuerzo. Existen numerosas

metodologías de prueba de software. Las más utilizadas en las pruebas de módulos o componentes: ruta básica, caja

negra y caja blanca.

1. Prueba de la ruta básica. Consiste en desarrollar un conjunto de datos de prueba que asegure que cada instrucción o sentencia, del software, se ejecute al menos una vez. Las técnicas de teoría de grafos permiten

identificar tales conjuntos de datos. Por consiguiente, aunque puede ser imposible asegurar que cada camino

a través de un sistema de software sea comprobado, es posible asegurar que cada sentencia dentro del sistema

se ejecute al menos una vez durante el proceso de pruebas.

2. Pruebas de caja negra. Se aplican a la interfaz del software. No se conocen los detalles internos, se conocen

las funciones específicas para las que se diseñó el producto (módulo de software), por lo cual examina su

aspecto funcional. En esencia, las pruebas de caja negra se ejecutan desde el punto de vista del usuario y se

centran, no en el modo en que el software ejecuta su tarea, sino simplemente en que el software se ejecuta

correctamente en términos de exactitud y temporalidad (timeliness).

3. Pruebas de caja blanca. En este caso se conocen los detalles internos de la implementación y, por consiguiente, los diferentes caminos de ejecución. Las pruebas se basan en un examen próximo al detalle procedimental;

se prueban las rutas lógicas del software y la colaboración entre componentes, al proporcionar casos de prueba que utilicen conjuntos específicos de condiciones, bucles, o ambos.

En aplicaciones convencionales de software, éste se prueba desde dos perspectivas diferentes [Pressman 05:419]:

1. La lógica interna del programa se prueba mediante técnicas de diseño de casos de prueba de “caja blanca”;

2. Los requisitos de software se comprueban empleando técnicas de diseño de casos de prueba de “caja negra”.

En el caso de aplicaciones orientadas a objetos, la prueba empieza antes de la existencia del código fuente, pero

una vez generado éste, se diseñan una serie de pruebas para comprobar operaciones de una clase, relaciones con otras

clases, etcétera.

Niveles de las pruebas

Las pruebas del software se pueden realizar desde diferentes niveles teniendo presente el equipo humano que ha desarrollado el software.

• Pruebas de módulo o clase. Se prueban los módulos o clases, de modo independiente, y se realizan por el equipo que ha implementado el módulo. Preferentemente son pruebas de caja blanca.

• Pruebas de integración. Son realizadas por los técnicos que realizaron las fases de diseño e implementación y

son fundamentalmente pruebas de caja negra. Tratan de probar la interacción entre módulos.

• Pruebas de aplicación. Se aplican a los casos de construcción de software complejo y por esta razón suelen

realizarlas aquellos técnicos que desarrollaron la fase de análisis.

P:265

Resolución de problemas y desarrollo de software: Metodología de la programación 669

• Pruebas beta. Esta metodología cae dentro de la categoría de caja negra y se utiliza por los desarrolladores de

software de ambiente (entorno) PC. Esta metodología muy utilizada proporciona una versión preliminar del programa, a una audiencia especializada, denominada versión beta. El objetivo último es aprender cómo se ejecuta el software en situaciones de la vida real antes de lanzar al mercado la versión final del producto. Las

realiamentaciones de los usuarios —normalmente personas expertas— además, de la posible detección de

errores busca que los desarrolladores de software comiencen a diseñar productos compatibles con el producto

en prueba. Por ejemplo, en el caso de un nuevo sistema operativo, la distribución de una versión beta abierta

al desarrollo de software de utilidad (utilidades o utilerías) compatible, de modo que el sistema operativo final

aparece rodeado de aplicaciones actuales basadas en ese sistema operativo. Además, claro, las versiones beta

buscan impactar en el mercado con acciones específicas de mercadotecnia que influyen considerablemente en

las ventas.

18.7. MANTENIMIENTO

Cuando el producto software (el programa) se ha terminado, se distribuye entre los posibles usuarios, se instala en

las computadoras y se utiliza (producción). Sin embargo, y aunque, a priori, el programa funcione correctamente, el

software debe ser mantenido y actualizado. De hecho, el coste típico del mantenimiento excede, con creces, el coste

de producción del sistema original.

Un sistema de software producirá errores que serán detectados, casi con seguridad, por los usuarios del sistema

y que no se descubrieron durante la fase de prueba. La corrección de estos errores es parte del mantenimiento del

software. Otro aspecto de la fase de mantenimiento es la mejora del software añadiendo más características o modificando partes existentes que se adapten mejor a los usuarios.

Otras causas que obligarán a revisar el sistema de software en la etapa de mantenimiento son las siguientes:

(1) Cuando un nuevo hardware se introduce, el sistema puede ser modificado para ejecutarlo en un nuevo entorno; (2) Si cambian las necesidades del usuario, suele ser menos caro y más rápido modificar el sistema existente que

producir un sistema totalmente nuevo. La mayor parte del tiempo de los programadores de un sistema se gasta en el

mantenimiento de los sistemas existentes y no en el diseño de sistemas totalmente nuevos. Por esta causa, entre otras,

se ha de tratar siempre de diseñar programas de modo que sean fáciles de comprender y entender (legibles) y fáciles

de cambiar.

18.7.1. La obsolescencia: programas obsoletos

La última etapa en el ciclo de vida del software es la evolución del mismo, pasando por su vida útil hasta su obsolescencia o fase en la que el software se queda anticuado y es preciso actualizarlo o escribir un nuevo programa

sustitutorio del antiguo.

La decisión de dar de baja un software por obsoleto no es una decisión fácil. Un sistema grande representa una

inversión enorme de capital que parece, a primera vista, más barato modificar el sistema existente en vez de construir

un sistema totalmente nuevo. Este criterio suele ser, normalmente, correcto y por esta causa los sistemas grandes se

diseñan para ser modificados. Un sistema puede ser productivamente revisado muchas veces. Sin embargo, incluso

los programas grandes se quedan obsoletos por caducidad de tiempo al pasar una fecha límite determinada. A menos

que un programa grande esté bien escrito y adecuado a la tarea a realizar, como en el caso de programas pequeños,

suele ser más eficiente escribir un nuevo programa que corregir el programa antiguo.

18.7.2. Iteración y evolución del software

Las etapas de vida del software suelen formar parte de un ciclo o bucle, como su nombre sugiere y no son simplemente una lista lineal. Es probable, por ejemplo, que durante la fase de mantenimiento tenga que volver a las especificaciones del problema para verificarlas o modificarlas.

Obsérvese en la Figura 18.5 que las diferentes etapas rodean al núcleo documentación. La documentación no es

una etapa independiente como se puede esperar sino que está integrada en todas las etapas del ciclo de vida del software.

P:266

670 Fundamentos de programación

Mantenimiento

Diseño

Verificación

Especificaciones

Evolución

Producción

Pruebas Codificación

Figura 18.5. Etapas del ciclo de vida del software con la documentación como núcleo aglutinador.

18.8. PRINCIPIOS DE DISEÑO DE SISTEMAS DE SOFTWARE

El diseño de sistemas de software de calidad requiere el cumplimiento de una serie de características y objetivos. En

un sentido general, los objetivos a conseguir que se consideran útiles en el diseño de sistemas incluyen al menos los

siguientes principios:

1. Modularidad mediante diseño descendente.

2. Abstracción y ocultamiento de la información.

3. Modificabilidad.

4. Comprensibilidad y fiabilidad.

5. Interfaces de usuario.

6. Programación segura contra fallos.

7. Facilidad de uso.

8. Eficiencia.

9. Estilo de programación.

10. Depuración.

11. Documentación.

18.8.1. Modularidad mediante diseño descendente

Un principio importante que ayuda a tratar la complejidad de un sistema es la modularidad. La descomposición del

problema se realiza a través de un diseño descendente que a través de niveles sucesivos de refinamiento se obtendrán

diferentes módulos. Normalmente los módulos de alto nivel especifican qué acciones han de realizarse mientras que

los módulos de bajo nivel definen cómo se realizan las acciones.

La programación modular tiene muchas ventajas. A medida que el tamaño de un programa crece, muchas tareas

de programación se hacen más difíciles. La diferencia principal entre un programa modular pequeño y un programa

modular grande es simplemente el número de módulos que cada uno contiene, ya que el trabajo con programas modulares es similar y sólo se ha de tener presente el modo en que unos módulos interactúan con otros. La modularidad

tiene un impacto positivo en los siguientes aspectos de la programación:

• Construcción del programa. La descomposición de un programa en módulos permite que los diversos programadores trabajen de modo independiente en cada uno de sus módulos. El trabajo de módulos independientes convierte la tarea de escribir un programa grande en la tarea de escribir muchos programas pequeños.

• Depuración del programa. La depuración de programas grandes puede ser una tarea enorme, de modo que se

facilitará esa tarea al centrarse en la depuración de pequeños programas más fáciles de verificar.

P:267

Resolución de problemas y desarrollo de software: Metodología de la programación 671

• Legibilidad. Los programas grandes son muy difíciles de leer, mientras que los programas modulares son más

fáciles de leer.

• Eliminación de código redundante. Otra ventaja del diseño modular es que se pueden identificar operaciones

que suceden en muchas partes diferentes del programa y se implementan como subprogramas. Esto significa

que el código de una operación aparecerá sólo una vez, produciendo como resultado un aumento en la legibilidad y modificabilidad.

18.8.2. Abstracción y encapsulamiento

La complejidad de un sistema puede ser gestionado utilizando abstracción. La abstracción es un principio común que

se aplica en muchas situaciones. La idea principal es definir una parte de un sistema de modo que puede ser comprendido por sí mismo (esto es como una unidad) sin conocimiento de sus detalles específicos y sin conocimiento de

cómo se utiliza esta unidad a un nivel más alto.

Existen dos tipos de abstracciones: abstracción procedimental y abstracción de datos. La mayoría de los lenguajes de programación soportan este tipo de abstracción. Es aquella en que se separa el propósito de un subprograma

de su implementación. Una vez que se ha escrito un subprograma, se puede utilizar sin necesidad de conocer las

peculiaridades de sus algoritmos. Suponiendo que el subprograma esté documentado adecuadamente, se podrá utilizar con sólo conocer la cabecera del mismo y sus comentarios descriptivos; no necesitará conocer su código.

La modularidad —tratada anteriormente— y la abstracción procedimental se complementan entre sí. La modularidad implica la rotura de una solución en módulos; la abstracción procedimental implica la especificación de

cada módulo claramente antes de que se implemente. De hecho, lo importante es poder utilizar los subprogramas

predefinidos, tales como Writeln, Sqrt, etc., o bien los definidos por el usuario sin necesidad de conocer sus

algoritmos.

El otro tipo de abstracción es la abstracción de datos, soportada hoy día por diversos lenguajes Turbo Pascal,

C++, Ada-83, Ada-95. Modula-2, etc. El propósito de la abstracción de datos es aislar cada estructura de datos y sus

acciones asociadas. Es decir, se centra la abstracción de datos en las operaciones que se realizan sobre los datos en

lugar de cómo se implementan las operaciones. Supongamos, por ejemplo, que se tiene una estructura de datos Clientes, que se utiliza para contener información sobre los clientes de una empresa, y que las operaciones o acciones a

realizar sobre esta estructura de datos incluyen Insertar, Buscar y Borrar. El módulo, objeto o tipo abstracto de datos, TipoCliente es una colección de datos y un conjunto de operaciones sobre esos datos. Tales operaciones pueden añadir nuevos datos, buscar o eliminar datos. Estas operaciones constituyen su interfaz, mediante la cual se comunica con otros módulos u objetos.

Otro principio de diseño es la ocultación de la información. El propósito de la ocultación de la información es

hacer inaccesible ciertos detalles que no afecten a los otros módulos del sistema. Por consiguiente, el objeto y sus

acciones constituyen un sistema cerrado, cuyos detalles se ocultan a los otros módulos.

La abstracción identifica los aspectos esenciales de módulos y estructura de datos, que se pueden tratar como

cajas negras. La abstracción indica especificaciones funcionales de cada caja negra; es responsable de su vista externa o pública. Sin embargo, la abstracción ayuda también a identificar detalles de lo que se debe ocultar de la vista

pública —detalles que no están en las especificaciones pero deben ser privados—. El principio de ocultación de la

información no sólo oculta detalles dentro de la caja negra sino que también asegura que ninguna otra caja negra

pueda acceder a estos detalles ocultos. Por consiguiente, se deben ocultar ciertos detalles dentro de sus módulos y

TAD y hacerlos inaccesibles a los restantes módulos y TAD.

18.8.3. Modificabilidad

La modificabilidad se refiere a los cambios controlados de un sistema dado. Un sistema se dice que es modificable

si los cambios en los requisitos pueden adecuarse bien a los cambios en el código. Es decir, un pequeño cambio en

los requisitos en un programa modular normalmente requiere un cambio pequeño sólo en algunos de sus módulos;

es decir, cuando los módulos son independientes (esto es, débilmente acoplados) y cada módulo realiza una tarea

bien definida (esto es, altamente cohesivos). La modularidad aísla las modificaciones.

Las técnicas más frecuentes para hacer que un programa sea fácil de modificar son: uso de subprogramas y uso

de constantes definidas por el usuario.

El uso de procedimientos tiene la ventaja evidente, no sólo de eliminar código redundante, sino también hace al

programa resultante más modificable. Normalmente será un signo de mal diseño de un programa que pequeñas mo-

P:268

672 Fundamentos de programación

dificaciones a un programa requieran su reescritura completa. Un programa bien estructurado en módulos será más

fácilmente modificable; es decir, si cada módulo resuelve sólo una pequeña parte del problema global, un cambio

pequeño en las especificaciones del problema normalmente sólo afectará a unos pocos módulos y en consecuencia

eso facilitará su modificación.

Las constantes definidas por el usuario o con nombre son otro medio para mejorar la modificabilidad de un programa.

EJEMPLO 18.4

Los límites del rango de un array suelen ser definidos mejor mediante constantes con nombre que mediante constantes numéricas. Así la declaración de un array y proceso posterior mediante bucle típico es:

tipo

array [1..100] de enteros:TipoPuntos

...

desde i←1 hasta 100 hacer

//proceso de los elementos

fin_desde

El diseño más eficiente podría ser:

const

NumeroDeItems = 100

tipo

array [1..NumeroDeItems] de enteros:TipoPunto

...

desde i←1 hasta NumeroDeItems hacer

//proceso de los elementos

fin_desde

ya que cuando se desee cambiar el número de elementos del array sólo sería necesario cambiar el valor de la constante NumeroDeItems, mientras que en el caso anterior supondrá cambiar la declaración del tipo y el índice de bucle, mientras que en el segundo caso sólo el valor de la constante.

18.8.4. Comprensibilidad y fiabilidad

Un sistema se dice que es comprensible si refleja directamente una visión natural del mundo3

. Una característica de

un sistema eficaz es la simplicidad. En general, un sistema sencillo puede ser comprendido más fácilmente que uno

complejo.

Un objetivo importante en la producción de sistemas es el de la fiabilidad. El objetivo de crear programas fiables

ha de ser crítico en la mayoría de las situaciones.

18.8.5. Interfaces de usuario

Otro criterio importante a tener presente es el diseño de la interfaz del usuario. Algunas directrices a tener en cuenta

pueden ser:

• En un entorno interactivo se ha de tener en cuenta las preguntas posibles al usuario y sobre todo aquellas que

solicitan entradas de usuario.

• Es conveniente que se realicen eco de las entradas de un programa. Siempre que un programa lee datos, bien

de usuario a través de un terminal o de un archivo, el programa debe incluir los valores leídos en su salida.

• Etiquetar (rotular) la salida con cabeceras y mensajes adecuados.

3

Tremblay, Donrek y Bunt: Introduction to Computer Science. An Algorithmic approach, McGraw-Hill, 1989, pág. 440.

P:269

Resolución de problemas y desarrollo de software: Metodología de la programación 673

18.8.6. Programación segura contra fallos

Un programa es seguro contra fallos cuando se ejecuta razonablemente por cualquiera que lo utilice. Para conseguir

este objetivo se han de comprobar los errores en datos de entrada y en la lógica del programa.

Supongamos un programa que espera leer datos enteros positivos pero lee –25. Un mensaje típico a visualizar

ante este error suele ser:

Error de rango

Sin embargo, es más útil un mensaje tal como éste:

–25 no es un número válido de años

Por favor vuelva a introducir el número

Otras reglas prácticas a considerar son:

• No utilizar tipos subrango para detectar datos de entrada no válidos. Por ejemplo, si se desea comprobar que

determinados tipos nunca sean negativos, no ayuda mucho cambiar las definiciones de tipo globales a:

tipo

o..maxent:TipoNoNegativo

Bajo..Alto:TipoMillar

array[TipoMillar] de TipoNoNegativo:TipoTabla

//un array de este tipo contiene solo enteros no negativos

• Comprobar datos de entrada no válidos:

Leer(Numero)

...

si Numero >= 0

entonces agregar Numero a total

sino manejar el error.

fin_si

• Cada subprograma debe comprobar los valores de sus parámetros. Así, en el caso de la función SumaIntervalo que suma todos los enteros comprendidos entre m y n.

entero:función SumaIntervalo (E_entero:m,n)

{

precondicion: m y n son enteros tales que m <= n

postcondicion: Devuelve SumaIntervalo = m+(m+1)+...+n

m y n son inalterables

{

var entero: Suma, Indice

inicio

Suma ← 0

desde Indice ← m hasta n hacer

Suma ← Suma + Indice

fin_desde

devolver(Suma)

fin

18.8.7. Facilidad de uso

La utilidad de un sistema se refiere a su facilidad de uso. Esta propiedad ha de tenerse presente en todas las etapas

del ciclo de vida, pero es vital en la fase de diseño e implementación.

P:270

674 Fundamentos de programación

18.8.8. Eficiencia

El objetivo de la eficiencia es hacer un uso óptimo de los recursos del programa. Tradicionalmente, la eficiencia ha

implicado recursos de tiempo y espacio. Un sistema eficiente es aquel cuya velocidad es mayor con el menor espacio

de memoria ocupada. En tiempos pasados los recursos de memoria principal y de CPU eran factores claves a considerar para aumentar la velocidad de ejecución. Hoy en el año 2008 con las unidades procesadoras típicas de los PCs

representados en Pentium IV o Athlon con frecuencias de 1,5 GHz a 3 GHz y memorias centrales de 128 MB a 512

MB e incluso 1 GB, el factor eficiencia ya no se mide con los mismos parámetros de memoria y tiempo. Hoy día

debe existir un compromiso entre legibilidad, modificabilidad y eficiencia, aunque, con excepciones, prevalecerá la

legibilidad y facilidad de modificación.

18.8.9. Estilo de programación, documentación y depuración

Estas características hoy día son claves en el diseño y construcción de programas, por esta causa dedicaremos por su

especial importancia tres secciones independientes para tratar estos criterios de diseño.

18.9. ESTILO DE PROGRAMACIÓN

Una de las características más importantes en la construcción de programas, sobre todo los de gran tamaño, es el estilo

de programación. La buena calidad en la producción de programas tiene relación directa con la escritura de un programa, su legibilidad y comprensibilidad. Un buen estilo de programación suele venir con la práctica, pero el requerimiento de unas reglas de escritura del programa, al igual que sucede con la sintaxis y reglas de escritura de un lenguaje

natural humano, debe buscar esencialmente que no sólo sean legibles y modificables por las personas que lo han construido sino también —y esencialmente— puedan ser leídos y modificados por otras personas distintas. No existe una

fórmula mágica que garantice programas legibles, pero existen diferentes reglas que facilitarán la tarea y con las que

prácticamente suelen estar de acuerdo, desde programadores novatos a ingenieros de software experimentados.

Naturalmente, las reglas de estilo para construir programas claros, legibles y fácilmente modificables dependerá

del tipo de programación y lenguaje elegido. En el caso de los lenguajes orientados a procedimientos como C, Pascal

o Modula-2 es conveniente considerar las siguientes reglas de estilo.

Reglas de estilo de programación:

1. Modularizar un programa en partes coherentes (uso amplio de subprogramas).

2. Evitar variables globales en subprogramas.

3. Usar nombres significativos para identificadores.

4. Definir constantes con nombres al principio del programa.

5. Evitar el uso del goto y no escribir nunca código spaghetti.

6. Escribir subrutinas cortas que hagan una sola cosa y bien.

7. Uso adecuado de parámetros variable.

8. Usar declaraciones de tipos.

9. Presentación (comentarios adecuados).

10. Manejo de errores.

11. Legibilidad.

12. Documentación.

18.9.1. Modularizar un programa en subprogramas

Un programa grande que resuelva un problema complejo siempre ha de dividirse en módulos para ser más manejable.

Aunque la división no garantiza un sistema bien organizado será preciso encontrar reglas que permitan conseguir esa

buena organización.

Uno de los criterios clave en la división es la independencia; esto es, el acoplamiento de módulos; otro criterio

es que cada módulo debe ejecutar una sola tarea, una función relacionada con el problema. Estos criterios fundamentalmente son acoplamiento y cohesión de módulos, aunque existen otros criterios que no se tratarán en esta sección.

El acoplamiento se refiere al grado de interdependencia entre módulos. El grado de acoplamiento se puede utilizar para evaluar la calidad de un diseño de sistema. Es preciso minimizar el acoplamiento entre módulos, es decir,

P:271

Resolución de problemas y desarrollo de software: Metodología de la programación 675

minimizar su interdependencia. El criterio de acoplamiento es una medida para evaluar cómo un sistema ha sido

modularizado. Este criterio sugiere que un sistema bien modularizado es aquel en que los interfaces sean claros y

sencillos.

Otro criterio para juzgar un diseño es examinar cada módulo de un sistema y determinar la fortaleza de la ligadura (enlace) dentro de ese módulo. La fortaleza interna de un módulo, esto es, lo fuertemente (estrictamente) relacionadas que están entre sí las partes de un módulo; esta propiedad se conoce por cohesión. Un modelo cuyas partes

estén fuertemente relacionadas con cada uno de los otros se dice que es fuertemente cohesivo. Un modelo cuyas

partes no están relacionadas con otras se dice que es cohesivo débilmente.

Los módulos de un programa deben estar débilmente acoplados y fuertemente cohesionados.

Como regla general es conveniente utilizar subprogramas ampliamente. Si un conjunto de sentencias realiza una

tarea recurrente, repetitiva, identificable, debe ser un subprograma. Sin embargo, una tarea no necesita ser recurrente para justificar el uso de un subprograma.

18.9.2. Evitar variables globales en subprogramas

Una de las principales ventajas de los subprogramas es que pueden implementar el concepto de un módulo aislado.

El aislamiento se sacrifica cuando un subprograma accede a variables globales, dado que los efectos de sus acciones

producen los efectos laterales indeseados, normalmente.

En general, el uso de variables globales con subprogramas no es correcto. Sin embargo, el uso de la variable

global en sí no tiene por qué ser perjudicial. Así, si un dato es inherentemente importante en un programa que casi

todo subprograma debe acceder al mismo, entonces esos datos han de ser globales por naturaleza.

18.9.3. Usar nombres significativos para identificadores

Los identificadores que representan los nombres de módulos, subprogramas, funciones, tipos, variables y otros elementos, deben ser elegidos apropiadamente para conseguir programas legibles. El objetivo es usar interfaces significativos que ayuden al lector a recordar el propósito de un identificador sin tener que hacer referencia continua a

declaraciones o listas externas de variables. Hay que evitar abreviaturas crípticas.

Identificadores largos se deben utilizar para la mayoría de los objetos significativos de un programa, así como los

objetos utilizados en muchas posiciones, tales como, por ejemplo, el nombre de un programa usado frecuentemente.

Identificadores más cortos se utilizarán estrictamente para objetos locales: así i, j, k son útiles para índices de arrays

en un bucle, variables contadores de bucle, etc., y son más expresivos que Indice, VariableDeControl, etc.

Los identificadores deben utilizar letras mayúsculas y minúsculas. Cuando un identificador consta de dos o más

palabras, cada palabra debe comenzar con una letra mayúscula. Una excepción son los tipos de datos definidos por

el usuario, que suelen comenzar con una letra minúscula. Así identificadores idóneos son:

SalarioMes Nombre MensajeUsuario MensajesDatosMal

Algunas reglas que se pueden seguir son:

• Usar nombres para nombrar objetos de datos, tales como variables, constantes y tipos. Utilizar Salario mejor

que APagar o Pagar.

• Utilizar verbos para nombrar procedimientos. LeerCaracter, LeerSigCar, CalcularSigMov son procedimientos que realizan estas acciones mejor que SigCar o SigMov (siguiente movimiento).

• Utilizar formas del verbo “ser” o “estar” para funciones lógicas. SonIguales, EsCero, EsListo y EsVacio

se utilizan como variables o funciones lógicas.

Si SonIguales (A, B)

Los nombres de los identificadores de objetos deben sugerir el significado del objeto al lector del programa.

P:272

676 Fundamentos de programación

18.9.4. Definir constantes con nombres

Se deben evitar constantes explícitas siempre que sea posible. Por ejemplo, no utilizar 7 para el día de la semana o

3.141592 para representar el valor de la constante π. En su lugar, es conveniente definir constantes con nombre que

permite Pascal, tal como:

constante Pi = 3.141592

constante NumDiasSemana = 7

constante Longitud = 45

Este sistema tiene la ventaja de la facilidad para cambiar un valor determinado bien por necesidad o por cualquier

error tipográfico

constante Longitud = 200;

constante Pi = 3.141592654;

18.9.5. Evitar el uso de ir (goto)

Uno de los factores que más contribuyen a diseñar programas bien estructurados es un flujo de control ordenado que

implica los siguientes pasos:

1. El flujo general de un programa es adelante o directo.

2. La entrada a un módulo sólo se hace al principio y se sale sólo al final.

3. La condición para la terminación de bucles ha de ser clara y uniforme.

4. Los casos alternativos de sentencias condicionales han de ser claros y uniformes.

El uso de una sentencia goto casi siempre viola al menos una de estas condiciones. Además, es muy difícil verificar la exactitud de un programa que contenga una sentencia goto. Por consiguiente, en general, se debe evitar el

uso de goto. Hay, sin embargo, raras situaciones en las que se necesita un flujo de control excepcional. Tales casos

incluyen aquellos que requieren o bien que un programa termine la ejecución cuando ocurre un error, o bien que un

subprograma devuelve el control a su módulo llamador. La inclusión de la sentencia en algunos lenguajes de compiladores modernos como C# no implica para nada el uso de la sentencia goto y sólo en circunstancias muy excepcionales, ya comentadas a lo largo del libro, se debe recurrir a ella.

18.9.6. Uso adecuado de parámetros valor/variable

Un programa interactúa —se comunica— de un modo controlado con el resto del programa mediante el uso de parámetros. Los parámetros valor pasa los valores al subprograma, pero ningún cambio que el programa hace a estos

parámetros se refleja en los parámetros reales de retorno a la rutina llamadora. La comunicación entre la rutina llamadora y el subprograma es de un solo sentido; por esta causa, en el caso de módulos aislados, se deben utilizar

parámetros valor siempre que sea posible.

¿Cuándo es adecuado usar parámetros variable? La situación más evidente es cuando un procedimiento necesita

devolver valores a la rutina llamadora. Sin embargo, si el procedimiento necesita devolver sólo un único valor, puede ser más adecuado usar una función.

Los parámetros variable, cuyos valores permanecen inalterables hacen el programa más difícil de leer y más propenso a errores si se requieren modificaciones; no obstante, pueden mejorar la eficiencia. La situación es análoga a

utilizar una constante en lugar de una variable cuyo valor nunca cambia. Por consiguiente, se debe alcanzar un compromiso entre legibilidad y modificabilidad por un lado y eficiencia por otro. A menos que exista una diferencia

significativa en eficiencia, se tomará generalmente el aspecto de la legibilidad y modificabilidad.

El paso por valor implica implícitamente efectuar una copia en otro lugar de la memoria, por eso no se devuelven

los cambios. En Java los parámetros se pasan siempre por valor, no obstante, cuando lo que se pasa es un tipo referencia los valores almacenados en dicho tipo se pueden devolver modificados al programa llamador. La copia se

efectúa de la referencia, pero no de los datos referenciados.

P:273

Resolución de problemas y desarrollo de software: Metodología de la programación 677

18.9.7. Uso adecuado de funciones

En el caso de una función, ésta se debe utilizar siempre que se necesite obtener un único valor. Este uso corresponde

a la noción matemática de función. Por consiguiente, es muy extraño que una función realice una tarea diferente de

devolver un valor y no debe hacerlo.

Una función no debe hacer nada sino devolver el valor requerido. Es decir, una función nunca tiene un efecto

lateral.

¿Qué funciones tienen potencial para efectos laterales?

• Funciones con variables globales. Si una función referencia a una variable global, presenta el peligro de un

posible efecto lateral. En general, las funciones no deben asignar valores a variables globales.

• Funciones con parámetros variable. Un parámetro variable es aquel en que su valor cambiará dentro de la

función. Este efecto es un efecto lateral. En general, las funciones no deben utilizar parámetros variables. Si

se necesitan parámetros variables utilizar procedimientos.

18.9.8. Tratamiento de errores

Un programa diseñado ante fallos debe comprobar errores en las entradas y en su lógica e intentar comportarse bien

cuando los encuentra. El tratamiento de errores con frecuencia necesita acciones excepcionales que constituirán un

mal estilo en la ejecución normal de un programa. Por ejemplo, el manejo de funciones puede implicar el uso de

funciones con efectos laterales.

Un subprograma debe comprobar ciertos tipos de errores, tal como entradas no válidas o parámetros valor. ¿Qué

acción debe hacer un subprograma cuando se encuentra un error? Un sistema puede, en el caso de un procedimiento,

presentar un mensaje de error y devolver un indicador o bandera lógica a la rutina llamadora para indicarle que ha

encontrado una línea de datos no válida; en este caso, el procedimiento deja la responsabilidad de realizar la acción

apropiada a la rutina llamadora. En otras ocasiones es más adecuado que el propio subprograma tome las acciones

pertinentes —por ejemplo— cuando la acción requerida no depende del punto en que fue llamado el subprograma.

Si una función maneja errores imprimiendo un mensaje o devolviendo un indicador, viola las reglas contra efectos laterales dadas anteriormente.

Dependiendo del contexto, las acciones apropiadas pueden ir desde ignorar los datos erróneos hasta continuar la

ejecución para terminar el programa. En el caso de un error fatal que invoque la terminación, una ejecución de interrumpir puede ser el método más limpio para abortar. Otra situación delicada se puede presentar cuando se encuentra un error fatal en estructuras condicionales si-entonces-sino o repetitivas mientras, repetir. La primera acción puede ser llamar a un procedimiento de diagnóstico que imprima la información necesaria para

ayudarle a determinar la causa del error; pero después de que el procedimiento ha presentado toda esta información,

se ha de terminar el programa. Sin embargo, si el procedimiento de diagnóstico devuelve el control al punto en el

que fue llamado, debe salir de muchas capas de estructuras de control anidadas. En este caso la solución más limpia

es que la última sentencia del procedimiento de diagnóstico sea interrumpir.

18.9.9. Legibilidad

Para que un programa sea fácil de seguir su ejecución (la traza) debe tener una buena estructura y diseño, una buena elección de identificadores, buen sangrado y utilizar líneas en blanco en lugares adecuados y una buena documentación.

Como ya se ha comentado anteriormente se han de elegir identificadores que describan fielmente su propósito.

Distinguir entre palabras reservadas, tales como desde o procedimiento; identificadores estándar, tales como real

o entero, e identificadores definidos por el usuario. Algunas reglas que hemos seguido en el libro son:

• Las palabras reservadas se escriben en minúsculas negritas (en letra courier, en el libro).

• Los identificadores, funciones estándar y procedimientos estándar en minúsculas con la primera letra en mayúsculas (Escribir).

P:274

678 Fundamentos de programación

• Los identificadores definidos por el usuario en letras mayúsculas y minúsculas. Cuando un identificador consta de dos o más palabras, cada palabra comienza con una letra mayúscula (LeerVector, ListaNumeros).

Otra circunstancia importante a considerar en la escritura de un programa es el sangrado o indentación de las

diferentes líneas del mismo. Algunas reglas importantes a seguir para conseguir un buen estilo de escritura que facilite la legibilidad son:

• Los bloques deben ser sangrados suficientemente para que se vean claramente (3 a 5 espacios en blanco puede

ser una cifra aceptable).

• En una sentencia compuesta, las palabras inicio-fin deben estar alineadas:

inicio

< sentencia1 >

< sentencia2 >

.

.

.

< sentencian >

fin

• Sangrado consistente. Siempre sangrar el mismo tipo de construcciones de la misma manera. Algunas propuestas pueden ser:

mientras <condicion> hacer

inicio

<sentencia>

fin_mientras

Sentencias pseudocódigo

si<condicion> si <condicion> entonces

entonces <sentencia1> <sentencia1>

si_no <sentencia2> si_no

fin_si <sentencia2>

fin_si

si <condicion>

entonces

<sentencias>

sino

<sentencias>

fin_si

18.10. LA DOCUMENTACIÓN

Un programa (un paquete de software) de computadora necesita siempre de una documentación que permita a sus

usuarios aprender a utilizarlo y mantenerlo. La documentación es una parte importante de cualquier paquete de software y, a su vez, su desarrollo es una pieza clave en la ingeniería de software.

La documentación de un paquete de software se produce, normalmente, para dos fines. Uno, es explicar las características del software y describir cómo utilizarlo, y el otro propósito de la documentación es describir la composición interna del software, de modo que el sistema pueda ser mantenido a lo largo de su ciclo de vida. La primera

documentación se denomina documentación del usuario y la segunda documentación del sistema.

La documentación del usuario está diseñada para ser leída por el usuario del software, y, por consiguiente, tiende a ser no técnica. Hoy día, la documentación del usuario se reconoce como una herramienta de marketing. Una

buena documentación del usuario, combinada con una interfaz de usuario bien diseñada hace un paquete de software

accesible y, por consiguiente, aumenta sus ventas. Por esta razón muchos desarrolladores de software contratan a

escritores técnicos para producir esta parte del producto o bien proporcionan versiones preliminares de su productos

P:275

Resolución de problemas y desarrollo de software: Metodología de la programación 679

a autores independientes de modo que puedan escribir manuales del usuario que estén disponibles en las librerías

cuando se lanza el producto al público.

La documentación de usuario, tradicionalmente, toma el formato de libro en papel, aunque cada día es más frecuente incluir el manual en el menú Ayuda del propio programa o como libro electrónico en un CD o DVD.

La documentación del sistema es inherentemente más técnica que la documentación del usuario. Una componente importante de la documentación del sistema es la versión fuente de todos los programas del sistema. Es muy importante que estos programas sean presentados en formato muy legible; por esta razón se requiere un buen uso de la

sintaxis y de la gramática del lenguaje de programación de alto nivel, el uso de sentencias comentario adecuadas y

en los puntos notables del código, un diseño modular que permita a cada módulo ser presentado como una unidad

coherente. Así mismo se requiere seguir convenios de notación, sangrados en las líneas de programa, establecer diferencias en la escritura de nombres de variables, constantes, objetos, clases, etc., y convenios de documentación que

aseguren que todos los programas están documentados suficientemente.

Existen tres grupos de personas que necesitan conocer la documentación del programa: programadores, operadores y usuarios. Los requisitos necesarios para cada uno de ellos suelen ser diferentes, en función de las misiones de

cada grupo:

programadores manual de mantenimiento del programa.

operadores manual del operador.

operador: persona encargada de correr el programa, introducir datos y extraer resultados.

usuario manual del usuario

usuario: persona o sección de una organización que explota el programa, conociendo

su función, las entradas requeridas, el proceso a ejecutar y la salida que produce.

En entornos interactivos, las misiones del usuario y operador suelen ser las mismas. Así pues, la documentación

del programa se puede concretar a:

• Manual del usuario.

• Manual de mantenimiento.

18.10.1. Manual del usuario

La documentación de un paquete (programa) de software suele producirse con dos propósitos: “uno, es explicar las

funciones del software y describir el modo de utilizarlas (documentación del usuario) porque está diseñada para ser

leída por el usuario del programa; dos, describir el software en sí para poder mantener el sistema en una etapa posterior de su ciclo de vida (documentación del sistema o de mantenimiento)

4

”.

La documentación de usuario es un instrumento comercial importante. Una buena documentación de usuario hará

al programa más accesible y asequible.

La documentación del sistema o manual de mantenimiento es por naturaleza más técnica que la del usuario.

Antiguamente esta documentación consistía en los programas fuente finales y algunas explicaciones sobre la construcción de los mismos. Hoy día esto ya no es suficiente y es necesario estructurar y ampliar esta documentación.

La documentación del sistema abarca todo el ciclo de vida del desarrollo del software, incluidas las especificaciones originales del sistema y aquellas con las que se verificó el sistema, los diagramas de flujo de datos (DFD),

diagramas entidad-relación (DER), diccionario de datos y diagramas o cartas de estructura que representan la estructura modular del sistema.

El problema más grave que se plantea es la construcción práctica real de la documentación y su continua actualización. Durante el ciclo de vida del software cambian continuamente las especificaciones, los diagramas de flujo y

de E/R (Entidad/Relación) o el diagrama de estructura; esto hace que la documentación inicial se quede obsoleta o

incorrecta y por esta causa la documentación requiere una actualización continua de modo que la documentación

final sea lo más exacta posible y se ajuste a la estructura final del programa.

4

Brookshear, Glen J.: Introducción a las ciencias de la computación, Addison-Wesley, 1995, pág. 272.

P:276

680 Fundamentos de programación

El manual de usuario debe cubrir al menos los siguientes puntos:

• Órdenes necesarias para cargar el programa en memoria desde el almacenamiento secundario (disco) y arrancar

su funcionamiento.

• Nombres de los archivos externos a los que accede el programa.

• Formato de todos los mensajes de error o informes.

• Opciones en el funcionamiento del programa.

• Descripción detallada de la función realizada por el programa.

• Descripción detallada, preferiblemente con ejemplos, de cualquier salida producida por el programa.

18.10.2. Manual de mantenimiento (documentación para programadores)

El manual de mantenimiento es la documentación requerida para mantener un programa durante su ciclo de vida. Se

divide en dos categorías:

• Documentación interna.

• Documentación externa.

Documentación interna

Esta documentación cubre los aspectos del programa relativos a la sintaxis del lenguaje. Esta documentación está

contenida en los comentarios, encerrados entre llaves { } o bien paréntesis/asteriscos (* *), o, en una línea, precedido por //. Algunos tópicos a considerar son:

• Cabecera de programa (nombre del programador, fecha de la versión actual, breve descripción del programa).

• Nombres significativos para describir identificadores.

• Comentarios relativos a la función del programa, así como de los módulos que componen el programa.

• Claridad de estilo y formato [una sentencia por línea, indentación (sangrado)], líneas en blanco para separar

módulos (procedimientos, funciones, unidades, etc.).

• Comentarios significativos.

EJEMPLOS

var

real:Radio //entrada, radio de un círculo

...

//Calcular Area

Area ← Pi * Radio * Radio

Documentación externa

Documentación ajena al programa fuente, que se suele incluir en un manual que acompaña al programa. La documentación externa debe incluir:

• Listado actual del programa fuente, mapas de memoria, referencias cruzadas, etc.

• Especificación del programa: documento que define el propósito y modo de funcionamiento del programa.

• Diagrama de estructura que representa la organización jerárquica de los módulos que comprende el programa.

• Explicaciones de fórmulas complejas.

• Especificación de los datos a procesar: archivos externos incluyendo el formato de las estructuras de los registros, campos etc.

• Formatos de pantallas utilizados para interactuar con los usuarios.

• Cualquier indicación especial que pueda servir a los programadores que deben mantener el programa.

P:277

Resolución de problemas y desarrollo de software: Metodología de la programación 681

18.10.3. Reglas de documentación

Un programa bien documentado es aquel que otras personas pueden leer, usar y modificar. Existen muchos estilos

aceptables de documentación y, con frecuencia, los temas a incluir dependerán del programa específico. No obstante, señalamos a continuación algunas características esenciales comunes a cualquier documentación de un programa:

1. Un comentario de cabecera para el programa que incluye:

a) Descripción del programa: propósito.

b) Autor y fecha.

c) Descripción de la entrada y salida del programa.

d) Descripción de cómo utilizar el programa.

e) Hipótesis sobre tipos de datos esperados.

f) Breve descripción de los algoritmos globales y estructuras de datos.

g) Descripción de las variables importantes.

2. Comentarios breves en cada módulo similares a la cabecera del programa y que contenga información adecuada de ese módulo, incluyendo en su caso precondiciones y postcondiciones. Describir las entradas y cómo

las salidas se relacionan con las entradas.

3. Escribir comentarios inteligentes en el cuerpo de cada módulo que expliquen partes importantes y confusas

del programa.

4. Describir claramente y con precisión los modelos de datos fundamentales y las estructuras de datos seleccionadas para representarlas así como las operaciones realizadas para cada procedimiento.

Aunque existe la tendencia entre los programadores y sobre todo entre los principiantes a documentar los programas como última etapa, esto no es buena práctica, lo idóneo es documentar el programa a medida que se desarrolla.

La tarea de escribir un programa grande se puede extender por períodos de semanas o incluso meses. Esto le ha de

llevar a la consideración de que lo que resulta evidente ahora puede no serlo de aquí a dos meses; por esta causa,

documentar a medida que se progresa en el programa es una regla de oro para una programación eficaz.

Regla

Asegúrese de que siempre se corresponden los comentarios y el código. Si se hace un cambio importante en el

código, asegúrese de que se realiza un cambio similar en el comentario.

18.11. DEPURACIÓN

Una de las primeras cosas que se descubren al escribir programas es que un programa raramente funciona correctamente la primera vez. La ley de Murphy “si algo puede ser incorrecto, lo será” parece estar escrita pensando en la

programación de computadoras.

Aunque un programa funcione sin mensajes de error y produzca resultados, puede ser incorrecto. Un programa

es correcto sólo si se producen resultados correctos para todas las entradas válidas posibles. El proceso de eliminar

errores —bugs— se denomina depuración (debugging) de un programa.

Cuando el compilador detecta un error, la computadora visualiza un mensaje de error, que indica que se ha producido un error y cuál puede ser la causa posible del error. Desgraciadamente, los mensajes de error son, con frecuencia, difíciles de interpretar y son, a veces, engañosos. Los errores de programación se pueden dividir en tres

clases: errores de compilación (sintaxis), errores en tiempo de ejecución y errores lógicos.

18.11.1. Localización y reparación de errores

Aunque se sigan todas las técnicas de diseño dadas a lo largo del libro y en este capítulo, en particular, y cualquier

otras que haya obtenido por cualquier otro medio (otros libros, experiencias, cursos, etcétera) es prácticamente im-

P:278

682 Fundamentos de programación

posible e inevitable que su programa carezca de errores. Afortunadamente los programas modulares, claros y bien

documentados, son ciertamente más fáciles de depurar que aquellos que no lo son. Es recomendable utilizar técnicas

de seguridad contra fallos, que protejan contra ciertos errores e informen de ellos cuando se encuentran.

Con frecuencia el programador, pero sobre todo el estudiante de programación, está convencido de la bondad de

sus líneas de programa, sin pensar en las múltiples opciones que pueden producir los errores: el estado incorrecto

de una variable lógica, la entrada de una cláusula then o else, la salida imprevista de un bucle por un mal diseño

de su contador, etc. El enfoque adecuado debe ser seguir la traza de la ejecución del programa utilizando las facilidades de depuración del EID (Entorno Integrado de Desarrollo) o añadir sentencias de escritura que muestren cuál

fue la cláusula ejecutada. En el caso de condiciones lógicas, si la condición es falsa cuando se espera que es verdadera —como el mensaje de error puede indicar— entonces el siguiente paso es determinar cómo se ha convertido en

falsa.

¿Cómo se puede encontrar el punto de un programa en que algo se ha convertido en una cosa distinta a lo que se

había previsto? Se puede hacer el seguimiento de la ejecución de un programa o bien paso a paso a través de las

sentencias del programa o bien estableciendo puntos de ruptura (breakpoint). Se puede examinar también el contenido de una variable específica bien estableciendo inspecciones/observaciones (watches) o bien insertando sentencias

escribir temporales. La clave para una buena depuración es sencillamente utilizar estas herramientas que indiquen

lo que está haciendo el programa.

La idea principal es localizar sistemáticamente los puntos del programa que causan el problema. La lógica de un

programa implica que ciertas condiciones sean verdaderas en puntos diferentes del programa (recuerde que estas

condiciones se llaman invariantes). Un error (bug) significa que una condición que pensaba iba a ser verdadera no

lo es. Para corregir el error se debe encontrar la primera posición del programa en la que una de estas condiciones

difiera de sus expectativas. La inserción apropiada de puntos de ruptura, y de observación o inspección o sentencias

escribir en posiciones estratégicas de un programa —tal como entradas y salidas de bucles, estructuras selectivas

y subprogramas— sirven para aislar sistemáticamente el error.

Las herramientas de diagnóstico han de informarles si las cosas son correctas o equivocadas antes o después de

un punto dado del programa. Por consiguiente, después de ejecutar el programa con un conjunto inicial de diagnósticos se ha de poder seguir el error entre dos puntos. Por ejemplo, si el programa ha funcionado bien hasta la llamada al procedimiento o función P1, pero algo falla cuando se llama al procedimiento P2, nos permite centrar el problema entre estos dos puntos, la llamada a P2 y el punto concreto donde se ha producido el error en P2. Este

método es muy parecido al de aproximaciones sucesivas, es decir, ir acotando la causa posible de error hasta limitarla a unas pocas sentencias.

Naturalmente la habilidad para situar los puntos de ruptura, de observación o sentencias escribir dependerá

del dominio que se tenga del programa y de la experiencia del programador. No obstante, le damos a continuación

algunas reglas prácticas que le faciliten su tarea de depuración.

Uso de sentencias escribir

Las sentencias escribir pueden ser muy adecuadas en numerosas ocasiones. Tales sentencias sirven para informar

sobre valores de variables importantes y la posición en el programa en que las variables toman esos valores. Es conveniente utilizar un comentario para etiquetar la posición.

{Posicion una}

escribir('Esta situado en posicion una del procedimiento Test')

escribir('A=', a, 'B = ', b, 'C = ', c)

18.11.2. Depuración de sentencias si-entonces-sino

Situar una parte de ruptura antes de una sentencia si-entonces-sino y examinar los valores de las expresiones

lógicas y de sus variables. Se pueden utilizar o bien puntos de ruptura o sentencias escribir para determinar qué

alternativa de la sentencia si se toma:

//Examinar valores de <condicion> y variables antes de si

si <condicion> entonces

escribir('Condicion verdadera: siga camino');

...

P:279

Resolución de problemas y desarrollo de software: Metodología de la programación 683

si_no

escribir('Condicion falsa: siga camino');

fin_si

Depuración de bucles

Situar los puntos de ruptura al principio y al final del bucle y examinar los valores de las variables importantes.

//examinar valores de m y n antes de entrar al bucle

desde i = m hasta n hacer

//Examinar los valores de i y variables importantes

fin_desde

//Examinar los valores de m y n después de salir del bucle

Depuración de subprogramas

Las dos posiciones clave para situar los puntos de ruptura son al principio y al final de un subprograma. Se deben

examinar los valores de los parámetros en estas dos posiciones utilizando o bien sentencias de escritura o ventanas

de inspección u observación (watches).

Lecturas de estructuras de datos completos

Las variables cuyos valores son arrays u otras estructuras puede ser interesante examinarlas. Para ello se recurre a

escribir rutinas específicas de volcado (presentación en pantalla o papel) que ejecuten la tarea. Una vez diseñada la

rutina se llama a ella desde puntos diferentes según interesa a la secuencia de flujo de control del programa y los

datos que sean necesarios en cada caso.

18.11.3. Los equipos de programación

En la actualidad es difícil y raro que un gran proyecto de software sea implementado (realizado) por un solo programador. Normalmente, un proyecto grande se asigna a un equipo de programadores, que por anticipado deben coordinar toda la organización global del proyecto.

Cada miembro del equipo es responsable de un conjunto de procedimientos, algunos de los cuales pueden ser

utilizados por otros miembros del equipo. Cada uno de estos miembros deberá proporcionar a los otros las especificaciones de cada procedimiento, condiciones pretest o postest y su lista de parámetros formales; es decir, la información que un potencial usuario del procedimiento necesita conocer para poder ser llamado.

Normalmente, un miembro del equipo actúa como bibliotecario, de modo que a medida que un nuevo procedimiento se termina y comprueba, su versión actualizada sustituye la versión actualmente existente en la librería. Una

de las tareas del bibliotecario es controlar la fecha en que cada nueva versión de un procedimiento se ha incorporado a la librería, así como asegurarse de que todos los programadores utilizan la versión última de cualquier procedimiento.

Es misión del equipo de programadores crear librerías de procedimientos, que posteriormente puedan ser utilizadas en otras aplicaciones. Una condición importante que deben cumplir los procedimientos: estar comprobados y

ahorro de tiempo/memoria.

18.12. DISEÑO DE ALGORITMOS

Tras la fase de análisis, para poder solucionar problemas sobre una computadora debe conocerse cómo diseñar algoritmos. En la práctica sería deseable disponer de un método para escribir algoritmos, pero, en la realidad, no existe

ningún algoritmo que sirva para realizar dicha escritura. El diseño de algoritmos es un proceso creativo. Sin embargo, existen una serie de pautas o líneas a seguir que ayudarán al diseño del algoritmo (Tabla 18.1).

P:280

684 Fundamentos de programación

Tabla 18.1. Pautas a seguir en el diseño de algoritmos.

1. Formular una solución precisa del problema que debe solucionar el algoritmo.

2. Ver si existe ya algún algoritmo para resolver el problema o bien se puede adaptar uno ya existente (algoritmos conocidos).

3. Buscar si existen técnicas estándar que se puedan utilizar para resolver el problema.

4. Elegir una estructura de datos adecuada.

5. Dividir el problema en subproblemas y aplicar el método a cada uno de los subproblemas (diseño descendente).

6. Si todo lo anterior falla, comience de nuevo en el paso 1.

De cualquier forma, antes de iniciar el diseño del algoritmo es preciso asegurarse que el programa está bien definido:

• Especificaciones precisas y completas de las entradas necesarias.

• Especificaciones precisas y completas de la salida.

• ¿Cómo debe reaccionar el programa ante datos incorrectos?

• ¿Se emiten mensajes de error? ¿Se detiene el proceso?, etc.

• Conocer cuándo y cómo debe terminar un programa.

18.13. PRUEBAS (TESTING)

Aunque muchos programadores utilizan indistintamente los términos prueba o comprobación (testing) y depuración,

son, sin embargo, diferentes. La comprobación (pruebas) se refiere a las acciones que determinan si un programa

funciona correctamente. La depuración es la actividad posterior de encontrar y eliminar los errores (bugs) de un

programa. Las pruebas de ejecución de programas —normalmente— muestran claramente que el programa contiene

errores, aunque el proceso de depuración puede, en ocasiones, resultar difícil de seguir y comprender.

No obstante, el análisis anterior no significa que la comprobación sea imposible; al contrario, existen diferentes

metodologías formales para las comprobaciones de programas. Una filosofía adecuada para pruebas de programas

incluye las siguientes consideraciones:

1. Suponer que su programa tiene errores hasta que sus pruebas muestren lo contrario.

2. Ningún test simple de ejecución puede probar que un programa está libre de error.

3. Trate de someter al programa a pruebas duras. Un programa bien diseñado manipula entradas “con elegancia”.

Por este término se entiende que el programa no produce errores en tiempo de ejecución ni produce resultados

incorrectos; por el contrario, el programa, en la mayoría de los casos, visualizará un mensaje de error claro y

solicita de nuevo los datos de entrada.

4. Comenzar la comprobación antes de terminar la codificación.

5. Cambiar sólo una cosa cada vez.

La prueba de un programa ocurre cuando se ejecuta un programa y se observa su comportamiento.

Cada vez que se ejecuta un programa con algunas entradas se prueba a ver cómo funciona el trabajo para esa

entrada particular. Cada prueba ayuda a establecer que el programa cumple las especificaciones dadas.

Selección de datos de prueba

Cada prueba debe ayudar a establecer que el programa cumple las especificaciones dadas. Parte de la ciencia de ingeniería de software es la construcción sistemática de un conjunto de entradas de prueba que es idóneo a descubrir errores.

Para que un conjunto de datos puedan ser considerados como buenos datos de prueba, sus entradas para prueba

necesitan cumplir dos propiedades.

Propiedades de buenos datos de prueba

1. Se debe conocer qué salida correcta debe producir un programa para cada entrada de prueba.

2. Las entradas de prueba deben incluir aquellas entradas que probablemente originen más errores.

P:281

Resolución de problemas y desarrollo de software: Metodología de la programación 685

Se deben buscar numerosos métodos para encontrar datos de prueba que produzcan probablemente errores. El

primer método se basa en identificar y probar entradas denominadas valores externos, que son especialmente idóneos

para causar errores. Un valor externo o límite de un problema en una entrada produce un tipo diferente de comportamiento. Por ejemplo, suponiendo que se tiene una función ver_hora que tiene un parámetro hora y una precondición:

Precondición: Horas está comprendido en el rango 0-23.

Los dos valores límites de ver_hora son hora igual a 0 (dado que un valor menor de 0 es ilegal) y hora igual

a 23 (dado que un valor superior a 23-24, ... es ilegal). Puede ocurrir que la función se comporte de modo diferente

para horario matutino (0 a 11) o nocturno (12 a 23), entonces 11 y 12 serán valores extremos. Si se espera un comportamiento diferente para hora igual a 0, entonces 1 es un valor extremo. En general no existe una definición

precisa de valor extremo, pero debe ser aquel que muestre un comportamiento límite en el sistema.

Valores de prueba extremos

Si no se pueden probar todas las entradas posibles, probar al menos los valores extremos. Por ejemplo, si el

rango de entradas legales va de cero a un millón, asegúrese probar la entrada 0 y la entrada 1.000.000. Es

buena idea considerar también 0,1 y –1 como valores límites siempre que sean entradas legales.

Otra técnica de prueba de datos es la denominada perfilador que básicamente considera dos reglas:

1. Asegúrese de que cada línea de su código se ejecuta al menos una vez para algunos de sus datos de prueba.

Por ejemplo, puede ser una porción de su código que maneje alguna situación rara.

2. Si existe alguna parte de su código que a veces se salte totalmente, asegúrese, en ese caso, que existe al menos una entrada de prueba que salte realmente esta parte de su código. Por ejemplo, un bucle en el que el

cuerpo se ejecute, a veces, cero veces. Asegúrese de que hay una entrada de prueba que produce que el cuerpo del bucle se ejecute cero veces.

18.13.1. Errores de sintaxis (de compilación)

Un error de sintaxis o en tiempo de compilación se produce cuando existen errores en la sintaxis del programa, tales

como signos de puntuación incorrectos, palabras mal escritas, ausencia de separadores (signos de puntuación), o de

palabras reservadas. Si una sentencia tiene un error de sintaxis, no puede ser traducida y su programa no se ejecutará.

Los errores de sintaxis son detectados por el compilador.

44 Field identifier expected

Normalmente, los mensajes de error son fáciles de encontrar. El siguiente ejemplo (en Object Pascal) presenta

dos errores de sintaxis: el punto y coma que falta al final de la primera línea y la palabra WritaLn mal escrita debería ser WriteLn.

Suma:= 0

for I:= 0 to 10 do

Suma:= Suma + A[I];

WritaLn (Suma/10);

18.13.2. Errores en tiempo de ejecución

Los errores en tiempo de ejecución —o simplemente de ejecución— (runtime error) suceden cuando el programa

trata de hacer algo imposible o ilógico. Los errores de ejecución sólo se detectan en la ejecución. Errores típicos son:

la división por cero, intentar utilizar un subíndice fuera de los límites definidos en un array, etc.

x ← 1/N produce un error si N = 0

P:282

686 Fundamentos de programación

Los mensajes de error típicos son del tipo:

Run-Time error nnn at xxxx:yyyy

nnn número de error en ejecución.

xxxx:yyyy dirección del error en ejecución (segmento y desplazamiento).

Los errores de ejecución se dividen en cuatro categorías:

• errores DOS. (números de mensaje).

• errores I/0.

• errores críticos.

• errores fatales.

18.13.3. Errores lógicos

Los errores lógicos son errores del algoritmo o de la lógica del programa. Son difíciles de encontrar porque el compilador no produce ningún mensaje de error. Se producen cuando el programa es perfectamente válido y produce una

respuesta.

Calcular la media de todos los números leídos del teclado

Suma ← 0

desde i ← 0 hasta 10 hacer

Leer (Num)

Suma ← Suma + Num

fin_desde

Media ← Suma / 10

La media está calculada mal, ya que existen once números (0 a 10) y no diez como se ha escrito. Si se desea

escribir la sentencia

Salario ← Horas * Tasa

y se escribe

Salario ← Horas + Tasa

Es un error lógico (+ por *) ya que a priori el programa funciona bien y sería difícil, por otra parte, a no ser que

el resultado fuese obvio, detectar el error.

18.13.4. El depurador

Los EID (Entornos Integrados de Desarrollo) tienen un programa depurador disponible para ayudarle a depurar un

programa; el programa depurador le permite ejecutar su programa, una sentencia cada vez, de modo que se pueda

ver el efecto de la misma. El depurador imprime un diagnóstico cuando ocurre un error de ejecución, indica la sentencia que produce el error y permite visualizar los valores de variables seleccionadas en el momento del error. Asimismo se puede seguir la pista de los valores de variables seleccionadas durante la ejecución del programa (traza),

de modo que se pueda observar cómo cambian estas variables mientras el programa se ejecuta. Por último, se puede

pedir al depurador que detenga la ejecución en determinados puntos (breakpoints); en esos momentos se pueden

inspeccionar los valores de las variables seleccionadas a fin de determinar si son correctas.

El depurador tiene la gran ventaja de posibilitar la observación de los diferentes valores que van tomando las

variables dentro del programa.

P:283

Resolución de problemas y desarrollo de software: Metodología de la programación 687

18.14. EFICIENCIA

La eficiencia de un programa es una medida de cantidad de recursos consumidos por el programa. Tradicionalmente,

los recursos considerados han sido el tiempo de ejecución y/o el almacenamiento (ocupación del programa en memoria). Mientras menos tiempo se utilice y menor almacenamiento, el programa será más eficiente.

El tiempo y almacenamiento (memoria) de la computadora suelen ser costosos y por ello su ahorro siempre será

importante. En algunos casos la eficiencia es críticamente importante: control de una unidad de vigilancia intensiva

de un hospital —un retardo de fracciones de segundo puede ser vital en la vida de un enfermo—, un programa de

control de roturas en una prensa hidráulica —la no detección a tiempo podría producir grandes inundaciones—, etc.

Por el contrario, existirán otros casos en los que el tiempo no será factor importante: control de reservas de pasajeros

en una agencia de viajes.

La mejora del tiempo de ejecución y el ahorro en memoria se suelen conseguir con la mejora de los algoritmos

y sus programas respectivos. En ocasiones, un simple cambio en un programa puede aumentar la velocidad de ejecución considerablemente. Como muestra de ello analicemos el problema siguiente desde el punto de vista de tiempo de ejecución.

Buscar en un array o lista de enteros una clave dada (un entero)

tipo

array[Primero..Ultimo] de entero:ArrayLista

var

ArrayLista:Lista

...

J = Primero;

mientras (T<> Lista [J] y (J < Ultimo) hacer

J ← J + 1;

fin_mientras

si T = Lista [J] entonces

escribir('el elemento', T, 'esta en la lista')

si_no

escribir('el elemento', T, 'no esta en la lista)

fin_si

El bucle va comprobando cada elemento de la lista hasta que encuentra el valor de T o bien se alcanza el final

de la lista sin encontrar T.

Supongamos ahora que la lista de enteros está ordenada.

45 73 81 120 160 321 450

En este caso el bucle puede ser más eficiente si en lugar de la condición

(T <> Lista[J]) y (J < Ultimo)

se utiliza

(T > Lista[J]) y (J < Ultimo)

Ello se debe a que si T es igual a Lista[J], se ha encontrado el elemento, y si T es menor que Lista[J],

entonces sabemos que T será más pequeño que todos los elementos que le siguen. Tan pronto como se pruebe un

valor de T y resulte menor que su correspondiente Lista[J], esta condición será falsa y el bucle se terminará. De

este modo, y como término medio, se puede ahorrar alrededor de la mitad del número de iteraciones.

En el caso de que T no exista en la lista, el número de iteraciones de ambos algoritmos es igual, mientras que si

T no existe en la lista, el algoritmo 2 (con T > Lista[J]) reducirá el número de iteraciones en la mitad y, por

consiguiente, será más eficiente.

P:284

688 Fundamentos de programación

El listado y ejecución del programa con los dos procedimientos muestran cómo con un simple cambio (el operador < > por el operador > ) se gana notablemente en eficiencia, ya que reduce el tiempo de ejecución. Una aplicación

práctica que muestra la eficiencia es el siguiente escrito en Object Pascal y que tras su ejecución se observa esta

propiedad.

program Eficiencia;

{comparar dos algoritmos de busqueda}

const

Primero = 1;

Ultimo = 10;

type

Indice = Primero..Ultimo;

Items = array [Indice] of integer;

var

Lista: Items;

J, T: integer;

procedure Busqueda1 (L: Items, T: Integer);

var

I: Indice;

begin

I:= Primero;

while (T<> L [I]) and (I < Ultimo) do

I:= I+1;

if T = L[I] then

WriteLn (‘el elemento ',T,'esta en la lista')

else

WriteLn (‘el elemento ',T,'no esta en la lista');

WriteLn (‘Busqueda terminada’);

WriteLn (I,’iteraciones’)

end;

procedure Busqueda2 (L: Items, T: Integer);

var

I: Indice;

begin

I:= 1;

while (T > L[I]) and (I < Ultimo) do

I:= I + 1;

if T = L [I] then

WriteLn ('el elemento ', T,'esta en la lista')

else

WriteLn ('el elemento',T, 'no esta en la lista');

WriteLn ('Busqueda terminada');

WriteLn (I, 'iteraciones')

end;

begin {programa principal}

WriteLn ('Introduzca 10 enteros en orden ascendente:');

for J:= Primero to Ultimo do

Read (Lista[J]);

ReadLn;

WriteLn ('Introducir numero a buscar:');

ReadLn (T);

P:285

Resolución de problemas y desarrollo de software: Metodología de la programación 689

Busqueda1 (Lista,T);

Busqueda2 (Lista,T)

end.

Ejecución

Introduzca 10 enteros en orden ascendente:

2 5 8 12 23 37 45 89 112 234

Introducir numero a buscar:

27

el elemento 27 no esta en la lista

Busqueda1 terminada en

10 iteraciones

el elemento 27 no esta en la lista

Busqueda2 terminada en

6 iteraciones

18.14.1. Eficiencia versus legibilidad (claridad)

Las grandes velocidades de los microprocesadores (unidades centrales de proceso) actuales, junto con el aumento

considerable de las memorias centrales (cifras típicas usuales superan siempre los 640K), hacen que los recursos típicos tiempo y almacenamiento no sean hoy día parámetros fundamentales para la medida de la eficiencia de un

programa.

Por otra parte, es preciso tener en cuenta que —a veces— los cambios para mejorar un programa pueden hacerlo más difícil de comprender: poco legibles o claros. En programas grandes la legibilidad suele ser más importante

que el ahorro en tiempo y en almacenamiento en memoria. Como norma general, cuando la elección en un programa

se debe hacer entre claridad y eficiencia, generalmente se elegirá la claridad o la legibilidad del programa.

18.15. TRANSPORTABILIDAD

Un programa es transportable o portable si se puede trasladar a otra computadora sin cambios o con pocos cambios

apreciables. La forma de hacer un programa transportable es elegir como lenguaje de programación la versión estándar del mismo, en el caso de Pascal: ANSI/IEEE estándar e ISO estándar, en el caso de C y de C++ los estándares

reconocidos también por ANSI, es decir, ANSI C++. En el caso de Java y de C#, no existe más problema que la

elección del proveedor, dado que ambos lenguajes están sometidos a un proceso de estandarización y de hecho siguen

las normas que establecen sus fabricantes principales Sun Microsystems y Microsoft.

CONCEPTOS CLAVE

• Abstracción de datos.

• Abstracción procedimental.

• Ciclo de vida del software.

• Clase.

• Compatibilidad.

• Corrección.

• Diseño.

• Documentación.

• Eficiencia.

• Extensibilidad.

• Integridad.

• Módulos.

• Oculatación.

• Prueba.

• Reutilización.

• Robustez.

• Transportabilidad.

• Verificabilidad.

P:286

690 Fundamentos de programación

RESUMEN

El desarrollo de un buen sistema de software se realiza durante el ciclo de vida, que es el período de tiempo que se

extiende desde la concepción inicial del sistema hasta su

eventual retirada de la comercialización o uso del mismo.

Las actividades humanas relacionadas con el ciclo de vida

implican procesos tales como análisis de requisitos, diseño,

implementación, codificación, pruebas, verificación, documentación, mantenimiento y evolución del sistema y obsolescencia.

Diferentes herramientas ayudan al programador y al ingeniero de software a diseñar una solución para un problema dado. Algunas de estas herramientas son diseño descendente, abstracción procedimental, abstracción de datos,

ocultación de la información y programación orientada a

objetos.

Una abstracción procedimental separa el propósito de

un subprograma de su implementación. De modo similar,

la abstracción de datos se centra en las operaciones que se

ejecutan sobre los datos en vez de cómo se implementarán

las operaciones.

Durante la fase de diseño, la abstracción es uno de los

medios más importantes con los que se intenta hacer frente

a la complejidad. La innovación de la programación orientada a objetos no reside en la idea de escribir programas

utilizando una serie de abstracciones, sino en el uso de clases para gestionar dichas abstracciones.

Los lenguajes modernos facilitan la implementación de

abstracciones de datos mediante clases, que pueden definirse como colecciones estructuradas de implementaciones de

tipos abstractos cuya potencia reside en la herencia.

Las técnicas orientadas a objetos aumentan la productividad y fiabilidad del desarrollador y facilitan la reutilización y extensibilidad del código.

La construcción de software requiere el cumplimiento

de numerosas características. Entre ellas se destacan las

siguientes:

• Eficiencia. La eficiencia de un software es su capacidad

para hacer un buen uso de los recursos que manipula.

• Verificabilidad. La verificabilidad —facilidad de verificación de un software— es su capacidad para soportar los procedimientos de validación y de aceptar

juegos de test o ensayo de programas.

• Fácil de utilizar. Un software es fácil de utilizar si se

puede comunicar consigo de manera cómoda.

• Robustez. Capacidad de los productos software de

funcionar incluso en situaciones anormales.

Existen dos principios fundamentales para conseguir

esto: diseño simple y descentralización.

• Compatibilidad. Facilidad de los productos para ser

combinados con otros.

• Transportabilidad (portabilidad). La transportabilidad

o portabilidad es la facilidad con la que un software

puede ser transportado sobre diferentes sistemas físicos o lógicos.

• Integridad. La integridad es la capacidad de un software a proteger sus propios componentes contra los

procesos que no tenga el derecho de acceder.

• Corrección. Capacidad de los productos software de

realizar exactamente las tareas definidas por su especificación.

• Extensibilidad. Facilidad que tienen los productos de

adaptarse a cambios en su especificación.

• Reutilización. Capacidad de los productos de ser reutilizados, en su totalidad o en parte, en nuevas aplicaciones.

P:287

APÉNDICES

CONTENIDO

Apéndice A. Especificaciones de lenguaje algorítmico UPSAM 2.0

Apéndice B. Prioridad de operadores

Apéndice C. Código ASCII y Unicode

Apéndice D. Guía de sintaxis del lenguaje C

Bibliografía y recursos de programación

P:289

APÉNDICEA

Especificaciones del lenguaje

algorítmico UPSAM 2.0

A.2. Operadores

A.3. Estructura de un programa

A.4. Estructuras de control

A.5. Programación modular

A.6. Archivos

A.7. Variables dinámicas

A.8. Programación orientada a objetos

A.9. Conjunto de palabras reservadas y símbolos reservados

P:290

694 Fundamentos de programación

A.1. ELEMENTOS DEL LENGUAJE

A.1.1. Identificadores

Se pueden formar con cualquier carácter alfabético regional (no necesariamente ASCII estándar), dígitos (0-9) y el

símbolo de subrayado (_), debiendo empezar siempre por un carácter alfabético. Los nombres de los identificadores

son sensibles a mayúsculas y se recomienda que su longitud no sobrepase los 50 caracteres.

A.1.2. Comentarios

Existen dos tipos de comentarios: Comentarios de una sola línea, se utilizará la doble barra inclinada (//); este símbolo servirá para ignorar todo lo que aparezca hasta el final de la línea. Comentarios multilínea, podrán ocupar más

de una línea utilizando los caracteres { y }, que indicarán respectivamente el inicio y el final del comentario. Todos

los caracteres incluidos entre estos dos símbolos serán ignorados.

A.1.3. Tipos de datos estándar

Datos numéricos

• Enteros. Se considera entero cualquier valor numérico sin parte decimal, independientemente de su rango. Para

la declaración de un tipo de dato entero se utiliza la palabra reservada entero.

• Reales. Se considera real cualquier valor numérico con parte decimal, independiente de su rango o precisión.

Para la declaración de un tipo de dato real se utiliza la palabra reservada real.

Datos lógicos

Se utiliza la palabra reservada lógico en su declaración.

Datos de tipo carácter

Se utiliza la palabra reservada carácter en su declaración.

Datos de tipo cadena

Se utiliza la palabra reservada cadena en su declaración. A no ser que se indique lo contrario se consideran cadenas

de longitud variable. Las cadenas de caracteres se consideran como un tipo de dato estándar pero estructurado (se

podrá considerar como un array de caracteres).

A.1.4. Constantes de tipos de datos estándar

Numéricas enteras

Están compuestas por los dígitos (0..9) y los signos + y – utilizados como prefijos.

Numéricas reales

Los números reales en coma fija utilizan el punto como separador decimal, además de los dígitos (0..9), y el carácter de signo (+ y -). En los reales en coma flotante, la mantisa podrá utilizar los dígitos (0..9), el carácter de

signo (+ y -) y el punto decimal (.). El exponente se separará de la mantisa mediante la letra E y la mantisa estará

formada por el carácter de signo y los dígitos.

P:291

Especifi caciones del lenguaje algorítmico UPSAM 2.0 695

Lógicas

Sólo podrán contener los valores verdad(verdadero) y falso.

De carácter

Cualquier carácter válido del juego de caracteres utilizado, delimitados por los separadores ' o ".

De cadena

Secuencia de caracteres válidos del juego de caracteres utilizados, delimitados por los separadores ' o ".

A.2. OPERADORES

Operadores aritméticos

Operador Significado

+

*

/

div

mod

**

Menos unitario

Resta

Más unitario (suma)

Multiplicación

División real

División entera

Resto de la división entera

Exponenciación

El tipo de dato de una expresión aritmética depende del tipo de dato de los operandos y del operador. Con los

operadores +, -, * y ^, el resultado es entero si los operandos son enteros. Si alguno de los operandos es real, el resultado será de tipo real. La división real (/) devuelve siempre un resultado real. Los operadores mod y div devuelven

siempre un resultado de tipo entero.

Operadores de relación

Operador Significado

=

<

>

<=

>=

<>

Igual a

Menor que

Mayor que

Menor o igual que

Mayor o igual que

Distinto de

Los operandos deben ser del mismo tipo y el resultado es de tipo lógico.

Operadores lógicos

Operador Significado

no

y

o

Negación lógica

Multiplicación lógica (verdadero si los dos operandos son verdaderos)

Suma lógica (verdadero si alguno de los operandos es verdadero)

Los operandos deben ser de tipo lógico y devuelven un operando de tipo lógico.

P:292

696 Fundamentos de programación

Operadores de cadena

Operador Significado

+

&

Concatenación de cadenas.

Concatenación de cadenas.

Trabajan con operandos de tipo cadena o carácter y el resultado siempre será de tipo cadena.

Prioridad básica de operadores

Primarios

Unarios

Multiplicativos

Aditivos

De cadena

De relación

( ) [ ] Paréntesis en expresiones o en llamadas a procedimientos o funciones. Corchetes en índices

de arrays.

-, +, no.

*, /, div, mod, y Exponenciación **.

+, -, o.

&, +.

=, <, >, <=, >=, <>.

Prioridad avanzada de operadores1

Los operadores se muestran en orden decreciente de prioridad de arriba a abajo. Los operadores del mismo grupo

tienen la misma prioridad (precedencia) y se ejecutan de izquierda a derecha o de derecha a izquierda según la asociatividad.

Operador Tipo Asociatividad

( )

( )

[ ]

.

Paréntesis.

Llamada a función.

Subíndice.

Acceso a miembros de un objeto.

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

++

--

Prefijo incremento.

Prefijo decremento.

Dcha-Izda

Dcha-Izda

+

-

!

~

(tipo)

nuevo (new)

Más unitario.

Menos unitario.

Negación lógica unitaria.

Complemento bit a bit unitario.

Modelado unitario.

Creación de objetos.

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

*

/

%

+

-

Producto.

División.

Resto entero.

Suma.

Resta.

Izda-Dcha

Izda-Dcha

Izda-Dcha

Izda-Dcha

Izda-Dcha

<<

>>

>>>

Desplazamiento bit a bit a la izquierda.

Desplazamiento bit a bit a la derecha con extensión de signo.

Desplazamiento bit a bit a la derecha rellenando con ceros.

Dcha-Izda

Dcha-Izda

Dcha-Izda

(continúa)

1

Estas reglas de prioridad se ajustan a lenguajes de programación modernos tales como C++, Java o C#.

P:293

Especifi caciones del lenguaje algorítmico UPSAM 2.0 697

(continuación)

Operador Tipo Asociatividad

<

<=

>

>=

instancia_de

(instance_of)

Menor que.

Menor o igual que.

Mayor que.

Mayor o igual que.

Verificación tipo de objeto.

Izda-Dcha

Izda-Dcha

Izda-Dcha

Izda-Dcha

Izda-Dcha

==

!=

Igualdad.

Desigualdad.

Izda-Dcha

Izda-Dcha

& AND (Y) bit a bit Izda-Dcha

^ OR (O) exclusive bit a bit. Izda-Dcha

| OR (O) inclusive bit a bit. Izda-Dcha

&& AND (Y) lógico. Izda-Dcha

|| OR (O) lógico. Izda-Dcha

?: Condicional ternario. Dcha-Izda

=

+=

-=

*=

/=

%=

&=

^=

|=

<<=

>>=

>>>=

Asignación.

Asignación de suma.

Asignación de resta.

Asignación de producto.

Asignación de división.

Asignación de módulo.

Asignación AND bit a bit.

Asignación OR exclusive bit a bit.

Asignación or inclusive bit a bit.

Asignación de desplazamiento a izquierda bit a bit.

Desplazamiento derecho bit a bit con asignación de extensión de signo.

Desplazamiento derecho bit a bit con asignación de extensión a cero.

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

A.3. ESTRUCTURA DE UN PROGRAMA

algoritmo <nombre_del_algoritmo>

//Secciones de declaraciones

[const

//declaraciones de constantes]

[tipos

//declaraciones de tipos]

[var

//declaraciones de variables]

//Cuerpo del programa

inicio

...

fin

A.3.1. Declaración de tipos de datos estructurados

Arrays

array[<dimensión>…] de <tipo_de_dato> : <nombre_del_tipo>

P:294

698 Fundamentos de programación

<dimensión> es un subrango con el índice del límite inferior y el límite superior. Por ejemplo,

array[5..20] de entero declararía un array de 16 elementos enteros. Pueden

aparecer varios separados por comas para declarar arrays de más de una dimensión.

<tipo_de_dato> es el identificador de cualquier tipo de dato estándar o definido por el usuario.

<nombre_del_tipo> es un identificador válido que se utilizará para referenciar el tipo de dato.

El acceso a un elemento de un array se realizará indicando su índice entre corchetes. El índice será una expresión

entera.

Registros

registro : <nombre_del_tipo>

<tipo_de_dato> : <nombre_del_campo>

fin_registro

<tipo_de_dato> es el identificador de cualquier tipo de dato estándar o definido por el usuario.

<nombre_del_tipo> es un identificador válido que se utilizará para referenciar el tipo de dato.

<nombre_del_campo> es un identificador válido que se utilizará para referenciar el campo del registro.

El acceso a un campo de una variable de tipo registro se realizará utilizando el carácter punto (.), por ejemplo,

MiRegistro.MiCampo.

Archivos secuenciales

archivo_s de <tipo_de_dato> : <nombre_del_tipo>

<tipo_de_dato> es el identificador de cualquier tipo de dato estándar o definido por el usuario.

<nombre_del_tipo> es un identificador válido que se utilizará para referenciar el tipo de dato.

Archivos directos

archivo_d de <tipo_de_dato> : <nombre_del_tipo>

<tipo_de_dato> es el identificador de cualquier tipo de dato estándar o definido por el usuario.

<nombre_del_tipo> es un identificador válido que se utilizará para referenciar el tipo de dato.

A.3.2. Declaración de constantes

Se realiza dentro de la sección de declaraciones de constantes.

<nombre_de_constante> = <expresión>

<nombre_de_constante> es un identificador válido que se utilizará para referenciar la constante.

<expresión> es una expresión válida. El tipo de la constante será el tipo de dato que devuelva la

expresión.

A.3.3. Declaración de variables

Se realiza dentro de la sección de declaraciones de variables.

<tipo_de_dato> : <nombre_de_variable>[= <expresión>]...

P:295

Especifi caciones del lenguaje algorítmico UPSAM 2.0 699

<tipo_de_dato> es el identificador de cualquier tipo de dato estándar o definido por el usuario.

<nombre_de_variable> es un identificador válido que se utilizará para referenciar la variable. En una declaración es posible declarar varias variables separadas por comas.

Es posible inicializar la variable en la declaración, <expresión> es una expresión válida del tipo de dato de la

variable.

A.3.4. Biblioteca de funciones

Funciones aritméticas

Función Significado

abs(x)

aleatorio()

arctan(x)

cos(x)

entero(x)

exp(x)

ln(x)

log10(x)

raiz2(x)

sen(x)

trunc(x)

Devuelve el valor absoluto de la expresión numérica x.

Devuelve un número aleatorio real mayor o igual que 0 y menor que 1.

Devuelve el arco tangente de x.

Devuelve el coseno de x.

Devuelve el primer valor entero menor que la expresión numérica x.

Devuelve el valor ex

.

Devuelve el logaritmo neperiano de x.

Devuelve el logaritmo en base 10 de x.

Devuelve la raíz cuadrada de x.

Devuelve el seno de x.

Trunca (elimina los decimales) de la expresión numérica x.

Funciones de cadena

Función Significado

longitud(c)

posición(c,sc)

subcadena(c,ini[,long])

Devuelve el número de caracteres de la cadena c.

Devuelve la posición de la primera aparición de la subcadena sc en la cadena.

Devuelve una subcadena de la cadena c formada por todos los caracteres a partir de la

posición ini. Si se incluye el argumento long, devuelve sólo los primeros long caracteres

a partir de la posición ini.

Funciones de conversión de número a cadena

Función Significado

código(car)

carácter(x)

valor(c)

cadena(x)

Devuelve el código ASCII del carácter car.

Devuelve el carácter correspondiente al código ASCII x.

Convierte la cadena c a un valor numérico. Si el contenido de la cadena c no puede convertirse

a un valor numérico (contiene caracteres alfabéticos, signos de puntuación inválidos, etc.),

devuelve 0.

Convierte a cadena el valor numérico x.

Funciones de información

Función Significado

tamaño_de(<variable>) Devuelve el tamaño en bytes de la variable.

P:296

700 Fundamentos de programación

A.3.5. Procedimientos de entrada/salida

leer(<lista_de_variables>) lee una o más variables desde la consola del sistema.

escribir(<lista_de_expresiones>) escribe una o más expresiones en la consola del sistema.

A.3.6. Instrucción de asignación

<variable> ← <expresión>

Primero evalúa el valor de la expresión y lo asigna a la variable. La variable y la expresión deben ser del mismo

tipo de dato.

A.4. ESTRUCTURAS DE CONTROL

A.4.1. Estructuras selectivas

Estructura selectiva simple y doble

si <expresión_lógica> entonces

<acciones>

[si_no

<acciones>]

fin_si

Estructura selectiva múltiple

según_sea <expresión> hacer

<lista_de_valores> : <acciones>

[si_no

<acciones>]

fin_según

<expresión> puede ser cualquier expresión válida de tipo ordinal.

<lista_de_valores> será uno o más valores separados por comas del mismo tipo que <expresión>.

La estructura verifica si el valor de la expresión coincide con alguno de los valores de la primera lista de valores;

si esto ocurre realiza las acciones correspondientes y el flujo de control sale de la estructura, en caso contrario evalúa

la siguiente lista. Las acciones de la cláusula si_no se ejecutará si ningún valor coincide con la <expresión>.

A.4.2. Estructuras repetitivas

Estructura mientras

mientras <expresión_lógica> hacer

<acciones>

fin_mientras

Estructura repetir

repetir

<acciones>

hasta_que <expresión_lógica>

P:297

Especifi caciones del lenguaje algorítmico UPSAM 2.0 701

Estructura desde

desde <variable> <valor_inicial> hasta <valor_final>

[incremento | decremento <valor_incremento>] hacer

<acciones>

fin_desde

<variable> puede ser cualquier variable en la que se pueda incrementar o decrementar su valor,

es decir todas las numéricas, las de tipo carácter y las lógicas.

<valor_inicial> es una expresión con el primer valor que toma la variable del bucle. Debe ser del

mismo tipo que la variable del bucle.

<valor_final> es una expresión con el último valor que toma la variable del bucle. Debe ser del

mismo tipo que la variable del bucle. El bucle finaliza cuando la variable toma un

valor mayor (o menor, si es decremental) que este valor inicial.

<valor_incremento> es una expresión con el valor en el que se incrementará o decrementará la variable

del bucle al final de cada iteración.

A.5. PROGRAMACIÓN MODULAR

A.5.1. Cuestiones generales

El ámbito de las variables declaradas dentro de un módulo (procedimiento o función) es local, y el tiempo de vida

de dicha variable será el tiempo de ejecución del módulo.

A.5.2. Procedimientos

Declaración

procedimiento <nombre_procedimiento>([<lista_parámetros_formales>])

[declaraciones locales]

inicio

fin_procedimiento

<nombre_procedimiento> debe ser un identificador válido.

<lista_parámetros_formales> son uno o más grupos de parámetros separados por punto y coma. Cada

grupo de argumentos se define de la siguiente forma:

{E | E/S} <tipo_de_dato> : <lista_de_parámetros>

E indica que el paso de parámetros se realiza por valor.

E/S indica que el paso de parámetros se realiza por referencia.

<tipo_de_dato> es un tipo de dato estándar o definido previamente por el usuario.

<lista_de_parámetros> es uno o más identificadores válidos separados por comas.

Llamada a procedimientos

[llamar_a] <nombre_procedimiento>([<lista_parámetros_actuales>])

La lista de parámetros actuales es una o varias variables o expresiones separadas por comas que deben coincidir

en número, orden y tipo con la lista de parámetros formales de la declaración.

P:298

702 Fundamentos de programación

A.5.3. Funciones

Declaración

<tipo_de_dato> : función <nombre_función>([<lista_parámetros_formales>])

[declaraciones locales]

inicio

devolver(<expresión>)

fin_función

<tipo_de_dato> es un tipo de dato estándar o definido previamente por el usuario. Se

trata del tipo del dato que devuelve la función.

<nombre_función> debe ser un identificador válido.

<lista_parámetros_formales> son uno o más grupos de parámetros separados por punto y coma.

Cada grupo de argumentos se define de la siguiente forma:

{E | E/S} <tipo_de_dato> : <lista_de_parámetros>

E indica que el paso de parámetros se realiza por valor.

E/S indica que el paso de parámetros se realiza por referencia.

<tipo_de_dato> es un tipo de dato estándar o definido previamente por el usuario.

<lista_de_parámetros> es uno o más identificadores válidos separados por comas.

<expresión> es el valor de retorno de la función. Debe coincidir con el tipo de dato de la declaración.

Llamada a funciones

<nombre_función>([<lista_parámetros_actuales>])

La lista de parámetros actuales es una o varias variables o expresiones separadas por comas que deben coincidir

en número, orden y tipo con la lista de parámetros formales de la declaración. Al devolver un valor y no existir funciones que no devuelven valores (excepto funciones void de C o Java), la llamada debe hacerse siempre dentro de

una expresión.

A.6. ARCHIVOS

A.6.1. Archivos secuenciales

Apertura del archivo

abrir(<variable_tipo_archivo,<modo_apertura>,<nombre_archivo>)

<var_tipo_archivo> es una variable de tipo archivo secuencial.

<modo_apertura> indica el tipo de operación que se realizará con el archivo. En el caso de archivos

secuenciales será:

• lectura, coloca el puntero al siguiente registro al comienzo del archivo y sólo realiza operaciones de lectura.

El archivo debe existir previamente.

• escritura, coloca el puntero al siguiente registro al comienzo del archivo y sólo realiza operaciones de escritura. Si el archivo no existe, primero crea un archivo vacío. Si el archivo existe, sobrescribe los datos que tenga.

• añadir, coloca el puntero al siguiente registro en la marca de final de archivo y sólo realiza operaciones de

escritura.

<nombre_archivo> es una expresión de cadena con el nombre que el sistema dará al archivo.

P:299

Especifi caciones del lenguaje algorítmico UPSAM 2.0 703

Cierre del archivo

cerrar(<lista_variables_tipo_archivo>)

Cierra el archivo o archivos abiertos previamente.

Entrada/salida

leer(<variable_tipo_archivo>, <variable>)

Leer del archivo abierto para lectura representado por <variable_tipo_archivo> el siguiente registro. El tipo

de la variable debe coincidir con el tipo base del archivo definido en la declaración del tipo de dato.

escribir(<variable_tipo_archivo>, <expresión>)

Escribe en el archivo abierto para escritura y representado por la variable de tipo archivo el valor de la expresión.

El tipo de la expresión debe coincidir con el tipo base del archivo definido en la declaración del tipo de dato.

A.6.2. Archivos de texto

Se considera el archivo de texto como un tipo especial de archivo compuesto de caracteres o cadenas. La declaración

de un tipo de dato de tipo archivo de texto sería, por tanto:

archivo_s de carácter : <nombre_tipo>

archivo_s de cadena : <nombre_tipo>

La lectura de un carácter único en un archivo de texto se haría de la forma leer(<variable_tipo_carácter>)

que leería el siguiente carácter del archivo. La lectura de una variable de tipo cadena (leer(<variable_tipo_cadena>)) leería todos los caracteres hasta el final de línea.

La escritura de datos en un archivo de texto también se podrá hacer carácter a carácter (escribir(<variable_

tipo_carácter>)) o línea a línea (escribir(<variable_tipo_cadena>)).

La detección del final de línea en un archivo de texto cuando se lee carácter a carácter se realizaría con la función

fdl:

fdl(<variable_tipo_archivo>)

La función fdl devuelve el valor lógico verdad, si el último carácter leído es el carácter de fin de línea.

A.6.3. Archivos directos

Apertura del archivo

abrir(<variable_tipo_archivo,<modo_apertura>,<nombre_archivo>)

<var_tipo_archivo> es una variable de tipo archivo directo.

<modo_apertura> indica el tipo de operación que se realizará con el archivo. En el caso de archivos

directos será:

• lectura, coloca el puntero al siguiente registro al comienzo del archivo y sólo realiza operaciones de lectura.

El archivo debe existir previamente.

• escritura, coloca el puntero al siguiente registro al comienzo del archivo y sólo realiza operaciones de escritura. Si el archivo no existe, primero crea un archivo vacío. Si el archivo existe, sobrescribe los datos que tenga.

• lectura/escritura, coloca el puntero al comienzo del archivo y permite operaciones tanto de lectura como

de escritura.

<nombre_archivo> es una expresión de cadena con el nombre que el sistema dará al archivo.

P:300

704 Fundamentos de programación

Cierre del archivo

cerrar(<lista_variables_tipo_archivo>)

Cierra el archivo o archivos abiertos previamente.

Acceso secuencial

leer(<variable_tipo_archivo>, <variable>)

Leer del archivo abierto para lectura representado por <variable_tipo_archivo> el siguiente registro. El tipo

de la variable debe coincidir con el tipo base del archivo definido en la declaración del tipo de dato.

escribir(<variable_tipo_archivo>, <expresión>)

Escribe en el archivo abierto para escritura y representado por la variable de tipo archivo el valor de la expresión.

El tipo de la expresión debe coincidir con el tipo base del archivo definido en la declaración del tipo de dato.

Acceso directo

leer(<variable_tipo_archivo>, <posición>, <variable>)

Lee el registro situado en la posición relativa <posición> y guarda su contenido en la variable.

escribir(<variable_tipo_archivo>, <posición>, <variable>)

Escribe el contenido de la variable en la posición relativa <posición>.

A.6.4. Consideraciones adicionales

Detección del final del archivo

Al cerrar un archivo abierto para escritura se coloca después del último registro la marca de fin de archivo. La función

fda permite detectar si se ha llegado a dicha marca.

fda(<variable_tipo_archivo>)

Devuelve el valor lógico verdad, si se ha intentado hacer una lectura secuencial después del último registro.

Determinar el tamaño del archivo

La función lda devuelve el número de bytes del archivo.

lda(<nombre_archivo>)

<nombre_archivo> es el nombre del archivo físico.

Para determinar el número de registros de un archivo se puede utilizar la expresión:

lda(<nombre_archivo>) / tamaño_de(<tipo_base_archivo>)

Otros procedimientos

borrar(<nombre_archivo>)

Elimina del disco el archivo representado por la expresión de cadena <nombre_archivo>. El archivo debe estar

cerrado.

P:301

Especifi caciones del lenguaje algorítmico UPSAM 2.0 705

renombrar(<nombre_archivo>,<nuevo_nombre>)

Cambia el nombre al archivo <nombre_archivo> por el de <nuevo_nombre>. El archivo debe estar cerrado.

A.7. VARIABLES DINÁMICAS

Declaración de tipos de datos dinámicos

puntero_a <tipo_de_dato> : <nombre_del_tipo>

Declara el tipo de dato <nombre_del_tipo> como un puntero a variables de tipo <tipo_de_dato>.

El valor constante nulo indica una referencia a un puntero nulo.

Referencia al contenido de una variable dinámica

<variable_dinamica>↑

Asignación y liberación de memoria con variables dinámicas

reservar(<variable_dinámica>)

Reserva espacio en memoria para una variable del tipo de dato del puntero y hace que la variable dinámica apunte a dicha zona.

liberar(<variable_dinamica>)

Libera el espacio de memoria apuntado por la variable dinámica. Dicha variable queda con un valor indeterminado.

A.8. PROGRAMACIÓN ORIENTADA A OBJETOS

A.8.1. Clases y objetos

Declaración de una clase

clase <nombre_de_clase>

//Declaración de atributos

//Declaración de constructores y métodos

fin_clase

<nombre_de_clase> es un identificador válido.

Declaración de tipos de referencias

<nombre_de_clase> : <nombre_de_referecia>

<nombre_de_clase> es el nombre de una clase previamente declarada.

<nombre_de_referencia> es un identificador válido que se utilizará para referenciar a un objeto de dicha

clase.

La declaración de una referencia a una clase se hará en la sección de declaraciones de variables o tipos de datos

de un algoritmo, o dentro de la sección de variables de otra clase.

P:302

706 Fundamentos de programación

Instanciación2

de clases

nuevo <nombre_de_constructor>([<argumentos_constructor>])

La declaración nuevo reserva espacio para un nuevo objeto de la clase a la que pertenece el constructor y devuelve una referencia a un objeto de dicha clase. <nombre_de_constructor> tendrá el mismo nombre de la clase

a la que pertenece. La llamada al constructor puede llevar argumentos para la inicialización de atributos (véase más

adelante en el apartado de constructores). La instanciación se puede realizar en una sentencia de asignación.

MiObjeto ← nuevo MiClase(arg1,arg2,arg3) //Dentro del código ejecutable

Referencias a miembros de una clase

NombreReferencia.nombreDeMiembro //Para atributos

NombreReferencia.nombreDeMiembro([listaParamActuales]) //Para métodos

Constructores

constructor <nombre_de_clase>[<lista_parametros_formales>])

//Declaración de variables locales

inicio

//Código del constructor

fin_constructor

Existe un constructor por omisión sin argumentos al que se le llama mediante <nombre_de_clase()>. Al igual

que con los métodos se admite la sobrecarga dentro de constructores, distinguiéndose los distintos constructores por

el orden, número y/o tipo de sus argumentos.

Puesto que la misión de un constructor es inicializar una instancia de una clase, la <lista_parámetros_formales> sólo incluyen argumentos de entrada, por lo que se puede omitir la forma en la que se pasan los argumentos.

Destructores

No se considera la existencia de destructores. Las instancias se consideran destruidas cuando se ha perdido una referencia a ellas (recolector de basura).

Visibilidad de las clases

Se consideran todas las clases como públicas, es decir, es posible acceder a los miembros de cualquier clase declarada en cualquier momento.

Referencia a la instancia desde dentro de la declaración de una clase

Es posible hacer referencia a una instancia desde dentro de una clase con la palabra reservada instancia que devuelve una referencia a la instancia de la clase que ha realizado la llamada al método. De esta forma instancia.

UnAtributo haría referencia al valor del atributo UnAtributo dentro de la instancia actual.

A.8.2. Atributos

Declaración de atributos

La declaración de los atributos de una clase se realizará dentro de la sección de declaraciones var de dicha

clase.

2

Crear instancias. El Diccionario de la Lengua Española no incluye este término. Se acepta en la obra por su uso en la jerga de programación.

P:303

Especifi caciones del lenguaje algorítmico UPSAM 2.0 707

const [privado|público|protegido]<tipo_de_dato>:<nombre>=<valor>

var

[privado|público|protegido][estático]

<tipo_de_dato> : <nombre_atributo> [ = <valor_inicial>]

<nombre_atributo> puede ser cualquier identificador válido.

<tipo_de_dato> puede ser cualquier tipo de dato estándar, definido por el usuario u otra clase declarada con anterioridad.

Es posible dar un valor inicial al atributo mediante una expresión de inicialización que deberá ser del mismo tipo

de dato que el atributo.

Las constantes son miembros estáticos, se enlaza en tiempo de compilación e indican que el valor no se puede

modificar.

Visibilidad de los atributos

Por omisión, se considera a los atributos privados, es decir, sólo son accesibles por los miembros de la clase. Para

que pueda ser utilizado por los miembros de otras clases de utilizará el modificador público. El modificador protegido se utiliza para que sólo pueda ser utilizado por los miembros de su clase y por los de sus clases hijas.

Atributos de clase (estáticos)

Un atributo que tenga el modificador estático no pertenece a ninguna instancia de la clase, sino que será común

a todas ellas. Para hacer referencia a un atributo de una clase se utilizará el nombre de la clase seguido del nombre

del atributo (MiClase.MiAtributoEstático).

Atributos constantes

El modificador const permite crear atributos constantes que no se modificarán durante el tiempo de vida de la instancia.

A.8.3. Métodos

Declaración de métodos

La declaración de métodos se realizará dentro de la clase después de la declaración de atributos sin indicar ninguna

sección especial.

[estático][abstracto][público|privado|protegido] <tipo_de_retorno>

método <nombre_del_método>(<lista_de_parámetros_formales>)

//declaración de variables

inicio

//Código

[devolver(<expresión>)]

fin_método

<nombre_del_método> es un identificador válido.

<tipo_de_retorno> es cualquier tipo de dato estándar, estructurado o una referencia a un objeto. La declaración devolver se utiliza para indicar el dato de retorno que devuelve la función

que debe coincidir con el tipo de retorno que aparece en la declaración. Si el método no devuelve valores se utilizará la palabra reservada nada y no aparecerá la palabra devolver.

P:304

708 Fundamentos de programación

La lista de parámetros formales se declararía igual que en los procedimientos y funciones. El paso de argumentos

se realizará como en los procedimientos y funciones normales.

Las variables locales se declararán en la sección var entre la cabecera del método y su cuerpo.

Visibilidad de los métodos

Por omisión, se consideran los métodos como públicos, es decir, es posible acceder a ellos desde cualquier lugar del

algoritmo. Para que pueda ser utilizado sólo por miembros de su clase se utilizará el modificador privado, en el

caso de los procedimientos, o privada, en el caso de las funciones. El modificador protegido o protegida se

utiliza para que sólo pueda ser utilizado por los miembros de su clase y por los de sus clases hijas.

Métodos estáticos

Un método que tenga el modificador estático o estática no pertenece a ninguna instancia de la clase, sino que

será común a todas ellas. Para hacer referencia a un método de una clase se utilizará el nombre de la clase seguido

del nombre del método (MiClase.MiMétodoEstático()).

Sobrecarga de métodos

Se permite la sobrecarga de métodos, es decir, la declaración de métodos con el mismo nombre pero con funcionalidades distintas. Para que en la llamada se pueda distinguir entre los métodos sobrecargados, el número, orden o tipo

de sus argumentos deben cambiar.

Ligadura de métodos

La ligadura de la llamada de un método con el método correspondiente se hace siempre de forma dinámica, es decir,

en tiempo de ejecución, con lo que se permite la existencia de polimorfismo.

A.8.4. Herencia

clase <clase_derivada> hereda_de[<especificador_acceso>]<superclase>

<clase_derivada> es un identificador válido.

<superclase> es una clase declarada anteriormente.

[<especificador_acceso>] establece el tipo de herencia (pública, protegida o privada). Si se omite se supone publica. Con el especificador de acceso omitido la clase derivada:

• Hereda todos los métodos y atributos de la superclase accesibles (atributos públicos y protegidos y métodos

públicos y protegidos) presentes sólo en la superclase.

• Sobreescribe todos los métodos y atributos de la superclase accesibles (atributos públicos y protegidos y métodos públicos y protegidos) presentes en ambas clases.

• Añade todos los métodos y atributos presentes sólo en la clase derivada.

Es posible acceder a atributos de la superclase o ejecutar sus métodos mediante la palabra reservada super.

• Referencia a un miembro de la superclase super.nombreMiembro().

• Referencia al constructor de la superclase: super().

Clases y métodos abstractos

Clases en las que algunos o todos los miembros no tienen implementación, por lo que no pueden instanciarse directamente. Servirán de clase base para clases derivadas.

abstracta clase <clase_base>

P:305

Especifi caciones del lenguaje algorítmico UPSAM 2.0 709

Aquellos métodos sin implementación se podrían declarar sin inicio ni fin de método.

abstracta TipoDato: método NombreMétodo ([paramFormales])

En estos casos, las clases hijas deberían implementar el método.

Herencia múltiple

clase <clase_derivada> hereda_de [<especificador_

de_acceso>]<superclase1>,...,

[<especificador_de_acceso>]<superclaseN>

//miembros

...

fin_clase

A.9. CONJUNTO DE PALABRAS RESERVADAS Y SÍMBOLOS RESERVADOS

Símbolo, palabra Traducción/Significado

-

&

=

*

.

.

/

//

[]

^

{

}

+

+

+

<

<=

<>

=

>

>=

abrir

abs(x)

abstracto

aleatorio()

algoritmo

añadir

arctan(x)

archivo_d

archivo_s

array

borrar

Menos unario (negativo).

Resta.

Concatenación.

Operador de asignación.

Referencia a una variable apuntada.

Multiplicación.

Cualificador de acceso a registros o a miembros de una clase.

Separador de decimales.

División real.

Comentario de una sola línea. Ignora todo lo que aparezca a continuación de la línea

Índice de array.

Exponenciación.

Inicio de comentario multilínea. Ignora todo lo que aparezca hasta encontrar el carácter de final de comentario (}).

Fin de comentario multilínea. Ignora todo lo que aparezca desde el carácter de inicio de comentario ({).

Comilla simple, delimitador de datos de tipo carácter o cadena.

Comilla doble, delimitador de datos de tipo carácter o cadena.

Más unario (positivo).

Suma.

Concatenación.

Menor que.

Menor o igual que.

Distinto de.

Igual a.

Mayor que.

Mayor o igual que.

Abre un archivo.

Devuelve el valor absoluto de la expresión numérica x.

Declaración de métodos abstractos (sin implementación).

Devuelve un número aleatorio real mayor o igual que 0 y menor que 1.

Inicio del pseudocódigo.

Modo de apertura de un archivo.

Devuelve el arco tangente de x.

Declaración de archivos directos.

Declaración de archivos secuenciales.

Declaración de arrays.

Borra un archivo del disco.

P:306

710 Fundamentos de programación

Símbolo, palabra Traducción/Significado

cadena

cadena(x)

carácter

carácter(x)

cerrar

clase

código(car)

const

const

constructor

cos(x)

decremento

desde

devolver

div

e

e

e/s

entero

entero(x)

entonces

escribir

escritura

estático

exp(x)

falso

fda

fdl

fin

fin_clase

fin_constructor

fin_desde

fin_función

fin_mientras

fin_procedimiento

fin_registro

fin_según

fin_si

función

hacer

hasta

hasta_que

hereda_de

incremento

inicio

instancia

lda

lectura

lectura/escritura

leer

liberar

ln(x)

log10(x)

longitud(c)

llamar_a

mientras

mod

string.

Convierte a cadena el valor numérico de x.

char.

Devuelve el carácter correspondiente al código ASCII de x.

Cierra uno o más archivos abiertos.

Inicio de la declaración de una clase.

Devuelve el código ASCII del carácter car.

Inicio de la sección de declaraciones de constantes.

Declaración de atributos constantes en la definición de clases.

Inicio de la declaración de un constructor.

Devuelve el coseno de x.

Decremento en estructuras repetitivas desde.

Inicio de estructura repetitiva desde, for.

Indica el valor de retorno de una función.

División entera.

Exponente.

Paso de argumentos por valor.

Paso de argumentos por referencia.

integer, int, long, byte, etc.

Devuelve el primer valor entero menor que la expresión numérica x.

then.

Escribe una o más expresiones en un dispositivo de salida (consola, archivo, etc.).

Modo de apertura de un archivo.

Declaración de atributos o métodos de clase o estáticos.

Devuelve el valor ex.

Falso, false.

Fin de archivo.

Fin de línea.

Fin de algoritmo.

Final de la declaración de una clase.

Fin de la declaración de un constructor.

Fin de estructura repetitiva desde.

Fin de la declaración de una función.

Fin de estructura repetitiva mientras.

Fin de un procedimiento.

Fin de la declaración de registro.

Fin de estructura selectiva múltiple.

end if, fin de estructura selectiva simple.

Inicio de la declaración de una función.

do.

to.

Fin de estructura repetitiva repetir.

Indica que una clase derivada hereda miembros de una superclase.

Incremento en estructuras repetitivas desde.

Inicio del código ejecutable de un algoritmo, módulo, constructor, etc.

Referencia a la instancia actual de la clase donde aparece.

Devuelve la longitud en bytes de un archivo.

Modo de apertura de un archivo.

Modo de apertura de un archivo.

Lee una o más variables desde un dispositivo de entrada (consola, archivo, etc.).

Libera el espacio asignado a una variable dinámica.

Devuelve el logaritmo neperiano de x.

Devuelve el logaritmo en base 10 de x.

Devuelve el número de caracteres de la cadena c.

Instrucción de llamada a un procedimiento.

while, inicio de estructura repetitiva mientras.

Módulo de la división entera.

P:307

Especifi caciones del lenguaje algorítmico UPSAM 2.0 711

Símbolo, palabra Traducción/Significado

nada

no

nuevo

nulo

o

posición(c,sc)

privado

procedimiento

protegido

público

puntero_a

raiz2(x)

real

registro

renombrar

repetir

reservar

según_sea

sen(x)

si

si_no

subcadena(c,ini[,long])

super

tamaño_de(x)

tipos

trunc(x)

valor(c)

var

verdad

y

Tipo de retorno de métodos que no devuelven valores, void.

Not.

Reserva espacio en memoria para un objeto de una clase y devuelve una referencia a dicho

objeto.

Constante de puntero nulo.

Operación lógica "o", "or".

Devuelve la posición de la primera aparición de la subcadena sc en la cadena c.

Modificador de acceso privado a un atributo o método.

Inicio de la declaración de un procedimiento.

Modificador de acceso a un atributo o método que permite el acceso a los miembros de su

clase y de las clases hijas.

Modificador de acceso público a un atributo o método.

Declaración de tipos de datos de asignación dinámica.

Devuelve la raíz cuadrada de x.

float, double, single, real, etc.

record, inicio de la declaración de registro.

Cambia el nombre de un archivo.

repeat, inicio de estructura repetitiva repetir.

Reserva espacio en memoria para una variable dinámica.

Inicio de estructura selectiva múltiple, case, select case, switch.

Devuelve el seno de x.

Inicio de estructura selectiva simple / doble, if.

else.

Devuelve una subcadena de la cadena c formada por todos los caracteres a partir de la

posición ini. Si se incluye el argumento long, devuelve sólo los primeros long caracteres

a partir de la posición ini.

Permite el acceso a miembros de la superclase.

Devuelve el tamaño en bytes de la variable x.

Inicio de la sección de declaraciones de tipos de datos.

Trunca (elimina los decimales) de la expresión numérica x.

Convierte la cadena c a un valor numérico. Si el contenido de la cadena c no puede convertirse

a un valor numérico (contiene caracteres alfabéticos, signos de puntuación inválidos, etc.),

devuelve 0.

Inicio de la sección de declaraciones de variables, o de la declaración de atributos de una

clase.

Verdadero, true.

Operación lógica "y", "and".

P:309

APÉNDICEB

Prioridad de operadores

P:310

714 Fundamentos de programación

PRIORIDAD DE OPERADORES (C/C++, JAVA)

Los operadores se muestran en orden decreciente de prioridad de arriba a abajo. Los operadores del mismo grupo

tienen la misma prioridad (precedencia) y se ejecutan de izquierda a derecha o de derecha a izquierda según asociatividad.

Operador Tipo Asociatividad

()

()

[]

.

Paréntesis.

Llamada a función.

Subíndice.

Acceso a miembros de un objeto.

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

++

--

Prefijo incremento.

Prefijo decremento.

Dcha-Izda

Dcha-Izda

+

-

!

~

(tipo)

new

Más unitario.

Menos unitario.

Negación lógica unitaria.

Complemento bit a bit unitario.

Modelado unitario.

Creación de objetos.

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

*

/

%

+

-

Producto.

División.

Resto entero.

Suma.

Resta.

Izda-Dcha

Izda-Dcha

Izda-Dcha

Izda-Dcha

Izda-Dcha

<<

>>

>>>

Desplazamiento bit a bit a la izquierda.

Desplazamiento bit a bit a la derecha con extensión de signo.

Desplazamiento bit a bit a la derecha rellenando con ceros.

Dcha-Izda

Dcha-Izda

Dcha-Izda

<

<=

>

>=

instanceof

Menor que.

Menor o igual que.

Mayor que.

Mayor o igual que.

Verificación tipo de objeto.

Izda-Dcha

Izda-Dcha

Izda-Dcha

Izda-Dcha

Izda-Dcha

==

!=

Igualdad.

Desigualdad.

Izda-Dcha

Izda-Dcha

& AND bit a bit. Izda-Dcha

^ OR exclusive bit a bit. Izda-Dcha

| OR inclusive bit a bit. Izda-Dcha

&& AND lógico. Izda-Dcha

|| OR lógico. Izda-Dcha

?: Condicional ternario. Dcha-Izda

=

+=

-=

*=

/=

%=

&=

Asignación.

Asignación de suma.

Asignación de resta.

Asignación de producto.

Asignación de división.

Asignación de módulo.

Asignación AND bit a bit.

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

P:311

Prioridad de operadores 715

Operador Tipo Asociatividad

^=

|=

<<=

>>=

>>>=

Asignación OR exclusive bit a bit.

Asignación or inclusive bit a bit.

Asignación de desplazamiento a izquierda bit a bit.

Desplazamiento derecho bit a bit con asignación de extensión de signo.

Desplazamiento derecho bit a bit con asignación de extensión a cero.

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

Dcha-Izda

P:313

APÉNDICEC

Códigos ASCII y Unicode

C.1. Código ASCII C.2. Código Unicode

P:314

718 Fundamentos de programación

C.1. CÓDIGO ASCII

El código ASCII (American Standard Code for Information Interchange; código estándar americano para intercambio

de información) es un código que traduce caracteres alfabéticos y caracteres numéricos, así como símbolos e instrucciones de control en un código binario de siete u ocho bits.

Tabla C.1. Código ASCII de la computadora personal PC

Valor ASCII Carácter Valor ASCII Carácter

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

Nulo

Sonido (pitido, bip)

Tabulación

Avance de línea

Cursor a inicio

Avance de página

Retorno de carro

π

§

▌ ↕

Cursor a la derecha

Cursor a la izquierda

Cursor arriba

Cursor abajo

Espacio

!

#

$

%

&

(

)

*

+

/

0

1

2

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

3

4

5

6

7

8

9

:

;

<

=

>

?

@

A

B

C

D

E

F

G

H

I

J

K

L

M

N

O

P

Q

R

S

T

U

V

W

X

Y

Z

[

\\

]

^

-

'

a

b

c

d

e

P:315

Códigos ASCII y Unicode 719

Valor ASCII Carácter Valor ASCII Carácter

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

f

g

h

i

j

k

l

m

n

o

p

q

r

s

t

u

v

w

x

y

z

{

¦

}

~

Q

ü

é

â

ä

à

å

ç

ê

ë

è

Ï

Î

Ì

Ä

Å

É

æ

Æ

ô

ö

ò

û

ù

ÿ

Ö

Ü

¢

£

Y♦

Pt

f

á

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

í

ó

ú

ñ

Ñ

a

o

¿

1/2

1/4

¡

«

»

|

Tabla C.1. Código ASCII de la computadora personal PC (continuación)

P:316

720 Fundamentos de programación

Valor ASCII Carácter Valor ASCII Carácter

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

α

β

Γ

π

Σ

σ

μ

τ

ϕ

θ

Ω

δ

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

ε

±

÷

°

n

2

(blanco 'FF')

C.1.1. Códigos ampliados de teclas

Los códigos ampliados de teclas se devuelven por esas teclas o combinaciones de teclas que no se pueden representar por los códigos ASCII listados en la Tabla C.1.

Tabla C.2. Códigos ampliados de teclas

Segundo código Significado

3

15

16-25

30-38

44-50

59-68

71

72

73

75

77

79

80

81

82

83

84-93

94-103

104-113

114

115

116

117

118

119

120-131

132

133

134

NULL (carácter nulo)

Shift Tab

Alt-Q/W/E/R/T/Y/U/I/O/P

Alt-A/S/D/F/G/H/I/J/K/L

Alt-Z/X/C/V/B/N/M

Teclas F1-F10

Home (Inicio)

Cursor arriba (↑)

PgUp (RePág)

Cursor a la izquierda (←)

Cursor a la derecha (→)

End (Fin)

Cursor abajo (↓)

PgDn (AvPág)

Ins

Del (Supr)

F11-F20 (Shift-F1 a Shift-F10)

F21-F30 (Ctrl-F1 hasta F10)

F31-F40 (Alt-F1 hasta F10)

Ctrl-PrtSc (Ctrl-ImprPant)

Ctrl-Flecha izquierda (Ctrl ←)

Ctrl-Flecha derecha (Ctrl→)

Ctrl-End (Ctrl-Fin)

Ctrl-PgDn (Ctrl-AvPág)

Ctrl-Home (Ctrl-Inicio)

Alt-1/2/3/4/5/6/7/8/9/0/–/=

Ctrl-PgUp (Ctrl-RePág)

F11

F12

Tabla C.1. Código ASCII de la computadora personal PC (continuación)

P:317

Códigos ASCII y Unicode 721

Segundo código Significado

135

136

137

138

139

140

Shift-F11 (Mayús-F11)

Shift-F12 (Mayús-F11)

Ctrl-F11

Ctrl-F12

Alt-F11

Alt-F12

C.1.2. Códigos de exploración de teclado

Los códigos de exploración de teclado son los códigos devueltos de las teclas en el teclado estándar de una computadora PC, tal como se ven por el compilador.

Estas teclas son útiles cuando se trabaja a nivel de lenguaje ensamblador. Los códigos de exploración de la tabla

se visualizan en valores hexadecimales (dígitos 0,1,2,…, 9, A,B,C,D,E,F).

Tabla C.3. Códigos de exploración del teclado

Tecla Código de exploración

en hexadecimal Tecla Código de exploración

en hexadecimal

Esc

¡l

@ 2

# 3

$ 4

% 5

ˆ 6

& 7

* 8

( 9

) 0

–-

+ =

Retroceso (Backspace)

Ctrl

A

S

D

F

G

H

J

K

F8

F9

F10

F11

F12

Scroll Lock (BloqDespl)

←/→

Q

W

+

1 End (Fin)

E

R

T

01

02

03

04

05

06

07

08

09

0A

0B

0C

0D

0E

1D

1E

1F

20

21

22

23

24

25

42

43

44

D9

DA

46

0F

10

11

4E

4F

12

13

14

Y

U

I

O

P

{ [

} ]

Return

/ \\

Z

X

L

.,

“’

~‘

←Shift (←Mayús)

Barra espaciadora

Caps Lock (BloqMayús)

F1

F2

F3

F4

F5

F6

F7

Signo menos

4 ←

5

6 →

PrtSc* (ImprPant)

Alt

C

V

B

N

M

<,

15

16

17

18

19

1A

1B

1C

2B

2C

2D

26

27

28

29

2A

39

3A

3B

3C

3D

3E

3F

40

41

4A

4B

4C

4D

37

38

2E

2F

30

31

32

33

Tabla C.2. Códigos ampliados de teclas (continuación)

P:318

722 Fundamentos de programación

Tecla Código de exploración

en hexadecimal Tecla Código de exploración

en hexadecimal

>.

?/

→Shift (→Mayús)

7 Home (Inicio)

8 (↑)

9 PgUp (Repág)

34

35

36

47

48

49

2 ↓

3 PgDn (AvPág)

0 Ins

Del (Supr)

Num Lock (BloqNum)

50

51

52

53

45

C.2. CÓDIGO UNICODE

Existen numerosos sistemas de codificación que asignan un número a cada carácter (letras, números, signos...). Ninguna codificación (el código ASCII es un ejemplo elocuente) específica puede contener caracteres suficientes. Por ejemplo,

la Unión Europea, por sí sola, necesita varios sistemas de codificación distintos para cubrir todos sus idiomas. También

presentan problemas de incompatibilidad entre los diferentes sistemas de codificación. Por esta razón se creó Unicode.

El consorcio Unicode es una organización sin ánimo de lucro que se creó para desarrollar, difundir y promover

el uso de la norma Unicode que especifica la representación del texto en productos y estándares de software modernos. El consorcio está integrado por una amplia gama de corporaciones y organizaciones de la industria de la computación y del procesamiento de la información (empresas tales como Apple, HP, IBM, Sun, Oracle, Microsoft,…

o estándares modernos tales como XML, Java, CORBA, etc.).

Formalmente, el estándar Unicode está definido en la última versión impresa del libro The Unicode Standard que

edita el consorcio y que también se puede “bajar” de su sitio Web.

En el momento de escribir este apéndice la última versión estándar ofrecida por el consorcio es la versión 3.2,

que se puede descargar de la Red en las direcciones que se indican a continuación.

Unicode está llamado a reemplazar al código ASCII y algunos de los restantes más populares, como Latin-1, en

unos pocos años y a todos los niveles. Permite no sólo manejar texto en prácticamente cualquier lenguaje utilizado

en el planeta, sino que también proporciona un conjunto completo y comprensible de símbolos matemáticos y técnicos que simplificará el intercambio de información científica.

Recomendamos al lector que visite los sitios Web que incluimos en esta página para ampliar la información que necesite en sus tareas de programación actuales o futuras. El código sigue evolucionando y dada la masiva cantidad de información que incluye, el mejor consejo es visitar estas páginas u otras similares, y si ya se ha convertido en un experto

programador y necesita el código a efectos profesionales, le recomendamos se descargue de la Red todo el código completo o adquiera en su defecto el libro que le indicamos a continuación que contiene toda la información oficial de Unicode.

Referencias Web

Página oficial del consorcio Unicode.

www.unicode.org

Información de Unicode en español

www.unicode.org/standard/translations/spanishhtml

Unicode para sistemas operativos Unix/Linux.

www.el.cam.ac.uk

Soporte Multilingue en Unicode para HTML, Fuentes, Navegadores Web y otras aplicaciones.

www.hclrss.demon.co.uk/unicode

Bibliografía

The Unicode Consortium: The Unicode Standard, Versión 3.0. Reading, MA, Addison-Wesley, 2000.

Tabla C.3. Códigos de exploración del teclado (continuación)

P:319

APÉNDICED

Guía de sintaxis del lenguaje C

D.1. Elementos básicos de un programa

D.2. Estructura de un programa C

D.3. El primer programa C ANSI

D.4. Palabras reservadas ANSI C

D.5. Directivas del preprocesador

D.6. Archivos de cabecera

D.7. Definición de macros

D.8. ComentarioS

D.9. Tipos de datos

D.10. Variables

D.11. Expresiones y operadores

D.12. Funciones de entrada y salida

D.13. Sentencias de control

D.14. Funciones

D.15. Estructuras de datos

D.16. Cadenas

D.17. Estructuras

D.18. Uniones

D.19. Campos de bits

D.20. Punteros (Apuntadores)

D.21. Preprocesador de C

P:320

724 Fundamentos de programación

D.1. ELEMENTOS BÁSICOS DE UN PROGRAMA

El lenguaje C fue desarrollado en Bell Laboratories para su uso en investigación y se caracteriza por un gran número de propiedades que lo hacen ideal para usos científicos y de gestión.

Una de las grandes ventajas del lenguaje C es ser estructurado. Se pueden escribir bucles que tienen condiciones de

entrada y salida claras y se pueden escribir funciones cuyos argumentos se verifican siempre para su completa exactitud.

Su excelente biblioteca estándar de funciones convierten a C en uno de los mejores lenguajes de programación

que los profesionales informáticos pueden utilizar.

D.2. ESTRUCTURA DE UN PROGRAMA C

Un programa típico en C se organiza en uno o más archivos fuentes o módulos. Cada archivo tiene una estructura

similar con comentarios, directivas de preprocesador, declaraciones de variables y funciones y sus definiciones. Normalmente se sitúan cada grupo de funciones y variables relacionadas en un único archivo fuente. Dentro de cada

archivo fuente, los componentes de un programa suelen colocarse en un determinado modo estándar. La Figura D.1

muestra la organización típica de un archivo fuente en C.

Comentarios

Directivas de preprocesador

Declaraciones

de variables y funciones

Definiciones de funciones

(cuerpo de las funciones)

Figura D.1. Organización de un programa C.

Los componentes típicos de un archivo fuente del programa son:

1. El archivo comienza con algunos comentarios que describen el propósito del módulo e información adicional,

tal como el nombre del autor y fecha y nombre del archivo. Los comentarios comienzan con /* y terminan

con */.

2. Órdenes al preprocesador, conocidas como directivas del preprocesador. Normalmente incluyen archivos de

cabecera y definición de constantes.

3. Declaraciones de variables y funciones visibles en todo el archivo. En otras palabras, los nombres de estas

variables y funciones se pueden utilizar en cualquiera de las funciones de este archivo. Si se desea limitar la

visibilidad de las variables y funciones sólo a ese módulo, ha de poner delante de sus nombres el prefijo static; por el contrario, la palabra reservada extern indica que los elementos se declaran y definen en otro

archivo.

4. El resto del archivo incluye definiciones de las funciones (su cuerpo). Dentro de un cuerpo de una función se

pueden definir variables que son locales a la función y que sólo existen en el código de la función que se está

ejecutando.

D.3. EL PRIMER PROGRAMA C ANSI

#include <stdio.h>

int main ()

{

printf ("¡Hola mundo!");

return 0;

}

P:321

Guía de sintaxis del lenguaje C 725

D.4. PALABRAS RESERVADAS ANSI C

auto double int struct

break else long switch

case enum register typedef

char extern return union

const float short unsigned

continue for signed void

default goto sizeof volatile

do if static while

D.5. DIRECTIVAS DEL PREPROCESADOR

El preprocesador es la parte del compilador que realiza la primera etapa de traducción o compilación de un archivo

C ANSI en instrucciones de máquina. El preprocesador procesa el archivo fuente y actúa sobre las órdenes, denominadas directivas de preprocesador, incluidas en el programa. Estas directivas comienzan con el signo de libra (almohadilla) #. Normalmente, el compilador invoca automáticamente al preprocesador antes de comenzar la compilación.

Se puede utilizar el preprocesador de tres formas distintas para hacer sus programas más modulares, más legibles y

más fáciles de personalizar:

1. Se puede utilizar la directiva #include para insertar el contenido de un archivo en su programa.

2. Mediante la directiva #define se pueden definir macros que permiten reemplazar una cadena por otra. Se

puede utilizar la directiva #define para dar nombres significativos a constantes numéricas, mejorando la

legibilidad de sus archivos fuente.

3. Con directivas tales como #if, #ifdef, #else y #endif pueden compilar sólo partes de su programa. Se

puede utilizar esta característica para escribir archivos fuente con código para dos o más sistemas, pero compilar sólo aquellas partes que se aplican al sistema informático en que se compila el programa.

D.6. ARCHIVOS DE CABECERA

Directivas tales como #include <stdio.h> indican al compilador que lea el archivo stdio.h demodo que sus

líneas se sitúan en la posición de la directiva. ANSI C soporta dos formatos para la directiva #include:

1. #include <stdio.h>

2. #include "demo.h"

El primer formato de #include lee el contenido de un archivo —el archivo estándar de C, stdio.h—. El segundo

formato visualiza el nombre del archivo encerrado entre las dobles comillas que está en el directorio actual.

D.7. DEFINICIÓN DE MACROS

Una macro define un símbolo equivalente a una parte de código C y se utiliza para ello la directiva #define. Se

pueden representar constantes tales como PI, IVA y BUFFER.

#define PI 3.14159

#define IVA 16

#define BUFFER 1024

que toman los valores 3.14159, 16 y 1.024, respectivamente. Una macro también puede aceptar un parámetro y

reemplazar cada ocurrencia de ese parámetro con el valor proporcionado cuando la macro se utiliza en un programa.

Por consiguiente, el código que resulta de la expansión de una macro puede cambiar dependiendo del parámetro que

P:322

726 Fundamentos de programación

se utilice cuando se ejecuta la macro. Por ejemplo, la macro siguiente acepta un parámetro y expande a una expresión

diseñada para calcular el cuadrado del parámetro.

#define cuadrado(x) ((x)*(x))

D.8. COMENTARIOS

El compilador ignora los comentarios encerrados entre los símbolos /* y */.

/* Mi primer programa */

Se pueden escribir comentarios multilínea.

/* Mi segundo programa C

escrito el día 15 de Agosto de 1985

en Carchelejo - Jaén - España */

Los comentarios no pueden anidarse. La línea siguiente no es legal:

/*Comentario /* comentario interno */ externo */

D.9. TIPOS DE DATOS

Los tipos de datos básicos incorporados a C son enteros, reales y carácter.

Tabla D.1. Tipos de datos enteros

Tipo de dato Tamaño en bytes Tamaño en bits Valor mínimo Valor máximo

signed char

unsigned char

signed short

unsignet short

signed int

unsigned int

signed long

unsigned long

1

1

2

2

2

2

4

4

8

8

16

16

16

16

32

32

–128

0

–32.768

0

–32.768

0

–2.147.483.648

0

127

255

32.767

65.535

32.767

65.535

2.147.483.647

4.294.967.295

El tipo char se utiliza para representar caracteres o valores integrales. Las constantes de tipo char pueden ser

caracteres encerrados entre comillas ('A', 'b', 'p'). Caracteres no imprimibles (tabulación, avance de página, etc.)

se pueden representar con secuencias de escape ('\\t', '\\f').

Tabla D.2. Secuencias de escape

Carácter Significado Código ASCII

\\a

\\b

\\f

\\h

\

\\t

\\v

\\\\

Carácter de alerta (timbre)

Retroceso de espacio

Avance de página

Nueva línea

Retorno de carro

Tabulación (horizontal)

Tabulación (vertical)

Barra inclinada

7

8

12

10

13

9

11

92

P:323

Guía de sintaxis del lenguaje C 727

Carácter Significado Código ASCII

\\?

\\'

\\''

\

nn

\\xnn

'\\0'

Signo de interrogación

Comilla

Doble comilla

Número octal

Número hexadecimal

Carácter nulo (terminación de cadena)

63

39

34

Tabla D.3. Tipos de datos de coma flotante

Tipo de dato Tamaño en bytes Tamaño en bits Valor mínimo Valor máximo

float

double

long double

4

8

10

32

64

80

3.4E – 38

1.7E – 308

3.4E – 4932

3.4E + 38

1.7E + 308

3.4E + 4932

Todos los números sin un punto decimal en programas C se tratan como enteros y todos los números con un

punto decimal se consideran reales de coma flotante de doble precisión. Si se desea representar números en base 16

(hexadecimal) o en base 8 (octal), se precede al número con el carácter 'OX' para hexadecimal y '0' para octal. Si

se desea especificar que un valor entero se almacena como un entero largo se debe seguir con una 'L'.

025 /* octal 25 o decimal 21 */

0x25 /* hexadecimal 25 o decimal 37 */

250L /* entero largo 250 */

D.10. VARIABLES

Todas las variables en C se declaran o definen antes de que sean utilizadas. Una declaración indica el tipo de una

variable. Si la declaración produce también almacenamiento (se inicia), entonces es una definición.

D.10.1. Nombres de variables en C

Los nombres de variables en C constan de letras, números y carácter subrayado. Pueden ser mayúsculas o minúsculas o una mezcla de tamaños. El tamaño de la letra es significativo. Las variables siguientes son todas diferentes.

Temperatura TEMPERATURA temperatura

A veces se utilizan caracteres subrayados y mezcla de mayúsculas y minúsculas para aumentar la legibilidad:

Dia_de_Semana DiaDeSemana Nombre_ciudad PagaMes

int x; /* declara x variable entera */

char nombre, conforme; /* declara nombre, conforme de tipo char */

int x = 0, no = 0; /* definen las variables x y no */

float total = 42.125; /* define la variable total */

Se pueden declarar variables múltiples del mismo tipo de dos formas. Así, una declaración

int v1; int v2; int v3; int v4;

Tabla D.2. Secuencias de escape (continuación)

P:324

728 Fundamentos de programación

o bien

int v1;

int v2;

int v3;

int v4;

pudiéndose declarar también de la forma siguiente:

int v1, v2, v3, v4;

C no soporta tipos de datos lógicos, pero mediante enteros se pueden representar: 0, significa falso; distinto de

cero, significa verdadero (cierto).

La palabra reservada const permite definir determinadas variables con valores constantes, que no se pueden

modificar. Así, si se declara:

const int z = 4350;

y si se trata de modificar su valor,

z = 3475;

el compilador emite un mensaje de error similar a “Cannot modify a const object in function main” (“No se puede modificar un objeto const en la función main”). Las variables declaradas como const pueden recibir valores

iniciales, pero no puede modificarse su valor con otras sentencias, ni utilizarse donde C espera una expresión

constante.

D.10.2. Variables tipo char

Las variables de tipo char (carácter) pueden almacenar caracteres individuales. Por ejemplo, la definición

char car = 'M';

declara una variable car y le asigna el valor ASCII del carácter M. El compilador convierte la constante carácter 'M'

en un valor entero (int), igual al código ASCII de 'M' que se almacena a continuación en el byte reservado para

car.

Dado que los caracteres literales se almacenan internamente como valores int, se puede cambiar la línea.

char car;

por

int car;

y el programa funcionará correctamente.

D.10.3. Constantes de cadena

Las cadenas de caracteres constan de cero o más caracteres separados por dobles comillas. La cadena se almacena

en memoria como una serie de valores ASCII de tipo char de un solo byte y se termina con un byte cero, que se

llama carácter nulo.

"Sierra Mágina en Jaén"

P:325

Guía de sintaxis del lenguaje C 729

Además de los caracteres que son imprimibles, se pueden guardar en constantes cadena, códigos de escape, símbolos especiales que representan códigos de control y otros valores ASCII no imprimibles. Los códigos de escape se

representan en la Tabla D.2 como un carácter único, almacenado internamente como un valor entero y compuesto de

una barra inclinada seguida por una letra, signo de puntuación o dígitos octales o hexadecimales. Por ejemplo, la

declaración

char c = '\

';

asigna el símbolo nueva línea a la variable C. En los PC, cuando se envía un carácter '\

' a un dispositivo de salida, o cuando se escribe '\

' en un archivo de texto, el símbolo nueva línea se convierte en un retorno de carro y un

avance de línea.

D.10.4. Tipos enumerados

El tipo enum es una “lista ordenada” de elementos como constantes enteras. A menos que se indique lo contrario,

el primer miembro de un conjunto enumerado de valores toma el valor 0, pero se pueden especificar valores. La declaración:

enum nombre {enum_1, enum_2,...}lista_variables;

enum diasSemana {Lunes, Martes, Miercoles, Jueves, Viernes, Sabado, Domingo};

significa que Lunes = 0, Martes = 1, etc. Sin embargo, si se hace Jueves = 10, entonces Lunes sigue siendo 0, Martes es igual a 2, etc.; pero ahora Viernes = 11, Sabado = 12, etc.

Un tipo enumerado se puede utilizar para declarar una variable

enum diasSemana Laborable;

y a continuación utilizarla con

Laborable = Jueves;

o bien

Laborable = Sabado;

if (Laborable >= Viernes)

printf ("Hoy no es laborable \

";

D.10.5. typedef

La sentencia typedef se utiliza para asignar un nombre nuevo a un tipo de dato derivado o básico. typedef no

define un nuevo tipo, sino simplemente un nombre nuevo para un tipo existente

typedef struct

{

float x;

float y;

} PUNTO;

La declaración de una variable de tipo PUNTO:

PUNTO origen = {0.0, 0.0}

P:326

730 Fundamentos de programación

D.10.6. Cualificadores de tipos const y volatile

La palabra reservada const se puede situar antes de una declaración de tipo para indicar al compilador que no se

puede modificar el valor

const int x5 = 100;

El modificador volatile indica explícitamente al compilador que el valor cambia (normalmente, de forma dinámica).

El cualificador volatile se utiliza en la siguiente declaración de puerto17 para asegurar que el compilador

evalúe siempre cualquier acceso indirecto a través del puntero

#define TTYPORT 0x17755U;

volatile char *puerto17 = (char *) TTYPORT;

*puerto17 = '0';

*puerto17 = 'N';

D.11. EXPRESIONES Y OPERADORES

Las expresiones son operaciones que realiza el programa.

a+b+c;

Tabla D.4. Operadores aritméticos

Operador Descripción Ejemplo

*

/

+

-

%

Multiplicación

División

Suma

Resta

Módulo

(a * b)

(a / b)

(a + b)

(a - b)

(a % b)

Tabla D.5. Operadores relacionales

Operador Descripción Ejemplo

<

<=

>

>=

==

!=

Menor que

Menor que o igual

Mayor que

Mayor o igual que

Igual

No igual

(a < b)

(a <= b)

(a > b)

(a >= b)

(a == b)

(a != b)

Tabla D.6. Operadores de incremento y decremento

Operador Descripción Ejemplo

++

––

Incremento en i

Decremento en i

++i, i++

––j, j––

P:327

Guía de sintaxis del lenguaje C 731

EJEMPLOS

++i; /* Sumar uno a i */

i++; /* Igual que anterior */

–-i: /* Resta uno a i */

i––; /* Igual que anterior */

Tabla D.7. Operadores de manipulación de bits (bitwise)

Operador Descripción Ejemplo

&

|

^

<<

>>

˜

AND bit a bit

OR inclusiva bit a bit

OR exclusiva bit a bit

Desplazar bits a izquierda

Desplazar bits a derecha

Complemento a uno

C = A&B;

C = A|B;

C = A^B;

C = A<<B;

C = A>>B;

C = ˜B

D.11.1. Operadores de asignación

Los operadores de asignación son binarios y combinaciones de operadores y del signo = se utiliza para abreviar expresiones:

A = B /* asigna el valor de B a A */

C = (A=B) /* C y A son iguales a B */

C = A =B /* asigna B a A y a C */

A = A + 45; equivale a A + = 45;

El compilador puede generar un código más eficiente, recurriendo a operadores de asignación compuestos del

tipo * =, + =, etc., cada operador compuesto (op) reduce la expresión en pseudocódigo:

a = a op b

a la forma abreviada

a op = b;

Tabla D.8. Operadores de asignación

Operador Descripción Ejemplo

=

* =

/ =

% =

+ =

- =

<< =

>> =

& =

^ =

| =

Operación de asignación simple

z *= 10;

z /= 5;

z %= 2;

z += 4;

z -= 5;

z <<= 3;

z >>= 4;

z &= j;

z ^= j;

z |= j;

equivale a

equivale a

equivale a

equivale a

equivale a

equivale a

equivale a

equivale a

equivale a

equivale a

a = b;

z = z * 10;

z = z / 5;

z = z % 2;

z = z + 4;

z = z - 5;

z = z << 3;

z = z >> 3;

z = z & j;

z = z ^ j;

z = z | j;

P:328

732 Fundamentos de programación

D.11.2. Operador serie

La expresión a,b se evalúa primero la expresión a, y a continuación, la expresión b. El tipo y valor de la expresión

es el de b.

El operador en serie, la coma, indica una serie de sentencias ejecutadas de izquierda a derecha. Se utiliza normalmente en bucles for. Por ejemplo:

for (cuenta=1; cuenta<100; ++cuenta, ++lineasporpagina);

produce el incremento de la variable cuenta y de la variable lineasporpagina cada vez que se ejecuta el bucle

(se realiza una iteración).

D.11.3. Operador condicional

Dadas las expresiones a, b y c, la expresión

a ? b : c

toma como valor b si a es distinto de cero, y c en caso contrario (las expresiones b y c deben ser del mismo tipo de

datos).

D.11.4. Operador de conversión de tipos

(tipo) a convierte a un tipo especificado

expresión

nombre de un tipo de dato básico, un tipo de dato enumerado, un tipo definido typedef o un tipo de dato deri vado.

EJEMPLO

(int)29.55 + (int)21.99 se evalúa en C como 29 + 21

(float)6 / (float)4 produce el resultado 1.5 (6/4)

D.11.5. Operador sizeof

sizeof(tipo) Toma como valor el número de bytes necesarios para contener un valor de un tipo especificado;

sizeof (int) proporciona 2 bytes.

syzeof a Toma como valor el número de bytes requeridos para contener el resultado de la evaluación de a.

D.11.6. Prioridad (precedencia) de operadores

Las expresiones C constan de diversos operandos y operadores. En expresiones complejas, las subexpresiones con operadores de prioridad (precedencia) más alta se evalúan antes que las subexpresiones con operadores de menor prioridad.

Tabla D.9. Orden de evaluación y prioridad de operadores (asociatividad)

Nivel Operadores Orden de evaluación

1

2

3

4

( ) . [ ] >

* & ! ~ ++ –– + -

(conversión de tipo) sizeof

* / %

+ -

izquierda-derecha

derecha-izquierda

izquierda-derecha

izquierda-derecha

P:329

Guía de sintaxis del lenguaje C 733

Nivel Operadores Orden de evaluación

5

6

7

8

9

10

11

12

13

14

15

<< >>

< <= > >=

= = !=

&

^

|

&&

||

?:

= *= /= += -= %= <<= >>= &= ^= |=

,

izquierda-derecha

izquierda-derecha

izquierda-derecha

izquierda-derecha

izquierda-derecha

izquierda-derecha

izquierda-derecha

izquierda-derecha

derecha-izquierda

derecha-izquierda

izquierda-derecha

D.12. FUNCIONES DE ENTRADA Y SALIDA

Las funciones printf() y scanf() permiten comunicarse con un programa. Se denominan funciones de E/S. printf() es una función de salida y scanf() es una función de entrada y ambas utilizan una cadena de control y una

lista de argumentos.

D.12.1. printf

La función printf escribe en el dispositivo de salida los argumentos de la lista de argumentos. Requiere el archivo

de cabecera stdio.h. La salida de printf se realiza con formato y su formato consta de una cadena de control y una

lista de datos.

printf (cadena de control [, item1, item2,...item]);

El primer argumento es la cadena de control (o formato); determina el formato de escritura de los datos. Los

argumentos restantes son los datos o variables de datos a escribir

printf ("Esto es una prueba %d\

", prueba);

La cadena de control tiene tres componentes: texto, identificadores y secuencias de escape. Se puede utilizar

cualquier texto y cualquier número de secuencias de escape. El número de identificadores ha de corresponder con el

número de variables o valores a escribir.

Los identificadores de la cadena de formato determinan cómo se escriben cada uno de los argumentos.

printf ("Mi pueblo favorito es Cazorla%s", msg);

cada identificador comienza con un signo porcentaje (%) y un código que indica el formato de salida de la va riable.

Tabla D.10. Códigos de identificadores

Identificador Formato

%d

%c

%s

%f

%e

%g

%lf

%u

%o

%x

Entero decimal

Carácter simple

Cadena de caracteres

Coma flotante (decimal)

Coma flotante (notación exponencial)

Usa el %f o el %e más corto

Tipo double

Entero decimal sin signo

Entero octal sin signo

Entero hexadecimal sin signo

Tabla D.9. Orden de evaluación y prioridad de operadores (asociatividad) (continuación)

P:330

734 Fundamentos de programación

Las secuencias de escape son las indicadas en la tabla

printf ("Mi flor favorita es la %s\

", msg);

printf ("La temperatura es %f grados centigrados \

", centigrados);

EJEMPLO D.1

#include <stdio.h>

#define PI 3.141593

#define SIERRA "Sierra Mágina"

int main (void)

{

printf ("El valor de PI es %f.\

", PI);

printf ("/%s/ \

", SIERRA);

printf ("/%-20s/ \

", SIERRA);

return 0;

}

La salida producida al ejecutar el programa es

El valor de PI es 3.141593.

/Sierra Magina/

/ Sierra Magina/

D.12.2. scanf

La función scanf() es la función de entrada con formato. Esta función se puede utilizar para introducir números

con formato de máquina, caracteres o cadena de caracteres, a un programa.

scanf ("%f", &fahrenheit);

El formato general de la función scanf() es una cadena de formato y uno o más variables de entrada. La cadena de control consta sólo de identificadores.

Tabla D.11. Identificadores de formato de scanf

Identificador Formato

%d

%c

%s

%f

%lf

%e

%ld

%u

%o

%x

%h

Entero decimal

Carácter simple

Cadena de caracteres

Coma flotante

Coma flotante en tipo double

Coma flotante

Entero largo

Entero decimal sin signo

Entero octal sin signo

Entero hexadecimal sin signo

Entero corto

Un ejemplo típico de uso de scanf es

printf ("Introduzca ciudad y provincia : ");

scanf ("%s %s", ciudad, provincia);

P:331

Guía de sintaxis del lenguaje C 735

Otros ejemplos de uso de scanf

scanf ("%d", &cuenta);

scanf ("%s", direccion);

scanf ("%d%d", &r, &c);

scanf ("%d*c%d", &x, &y);

D.13. SENTENCIAS DE CONTROL

Una sentencia consta de palabras reservadas, expresiones y otras sentencias. Cada sentencia termina con un punto y

coma (;).

Un tipo especial de sentencia, la sentencia compuesta o bloque, es un grupo de sentencias encerradas entre llaves ({...}). El cuerpo de una función es una sentencia compuesta. Una sentencia compuesta puede tener variables

locales.

D.13.1. Sentencia if

1. if (expresión) o bien if (expresión) sentencia;

sentencia;

2. Si la sentencia es compuesta

if (expresión) { o bien if (expresión)

{

sentencia1; sentencia1;

sentencia2; sentencia2;

... ...

} }

3. if (expresión = = valor)

sentencia;

4. if (expresión != 0) equivale a if (expresión)

sentencia;

Nota

(expresión != 0) y (expresión) son equivalentes, ya que cualquier valor distinto de cero representa cierto.

D.13.2. Sentencia if-else

1. if (expresión)

sentencia1;

else

sentencia2;

2. if (expresión) { o bien if (expresión)

{

sentencia1; sentencia1;

sentencia2; sentencia2;

... }

} else{ else

{

sentencia3; sentencia3;

sentencia4; sentencia4;

...

} }

P:332

736 Fundamentos de programación

D.13.2.1. Sentencias if anidadas

1. if (expresión1)

sentencia1;

else if (expresión2)

sentencia2;

else

sentencia3;

2. if (expresión1)

sentencia1;

else if (expresión2)

sentencia2;

else if (expresión3)

sentencia3;

else if (expresiónN)

sentenciaN;

else

SentenciaPorOmision; /* opcional */

D.13.3. Expresión condicional (?:)

Expresión condicional es una simplificación de una sentencia if-else. Su sintaxis es:

expresión1 ? expresión2 : espresión3;

que equivale a

if (expresión1)

expresión2;

else

expresión3;

EJEMPLO D.2

if (opcion == 'S') equivale a premio=(opcion == 'S')? 1000 : 0;

premio = 1000

else

premio = 0;

D.13.4. Sentencia switch

La sentencia switch realiza una bifurcación múltiple, dependiendo del valor de una expresión

switch (expresión) {

case valor1:

sentencia1; /*se ejecuta si expresión igual a valor1 */

break; /* salida de sentencia switch */

case valor2:

sentencia2;

break;

case valor3:

sentencia3;

break;

...

P:333

Guía de sintaxis del lenguaje C 737

default:

sentencia por omision; /* se ejecuta si ningún valor coincide con expresión */

}

EJEMPLO D.3

switch (op)

{

case 'a':

func1();

break;

case 'b':

func2();

break;

case 'H':

printf ("Hola \

");

default:

printf ("Salida \

");

};

D.13.5. Sentencia while

while (expresión) o bien while (expresión) {

sentencia; sentencia1;

sentencia2;

...

}

D.13.6. Sentencia do-while

do { o bien do {

sentencia; sentencia1;

... sentencia2;

} while (expresión); ...

} while (expresión)

Las sentencias do-while monosentencias se pueden escribir también así:

do sentencia; while (expresión);

D.13.7. Sentencia for

1. for (expresión1; expresión2; expresión3) {

sentencia;

}

2. for (expresión1; expresión2; expresión3) sentencia;

La sentencia for equivale a

expresión1;

while (expresión2) {

P:334

738 Fundamentos de programación

sentencia;

expresión3;

}

EJEMPLO D.4

a) for (i = 1; i <= 100; i++)

printf ("i = = %d\

", i);

b) for (i = 0, suma = 0; i <= 100; suma + = i, i++);

D.13.8. Sentencia nula

La sentencia nula representada por un punto y coma no hace nada. Se utilizan sentencias nulas en bucles, cuando

todo el proceso se hace en las expresiones del bucle en lugar del cuerpo. Por ejemplo, localizar el byte cero que

marca el final de una cadena:

char cad [80] = "Prueba";

int i;

for (i = 0; cad [i] != '\\0'; i++);

/* sentencia nula*/

D.13.9. Sentencia break

La sentencia break se utiliza para salir incondicionalmente de un bucle for, while, do-while o de una sección

case de una sentencia switch.

for (;;) {

...

if (expresion)

break;

}

D.13.10. Sentencia continue

La sentencia continue salta en el interior del bucle hasta el principio del bucle para proseguir con la ejecución,

dejando sin ejecutar las líneas restantes después de la sentencia continue hasta el final del bucle. continue es

similar a la sentencia break.

while (expresion1) {

...

if (expresion2)

continue;

...

}

D.13.11. Sentencia goto

Una sentencia goto dirige el programa a una sentencia específica que tiene etiqueta utilizada en dicha sentencia

goto. Las etiquetas deben terminar con un símbolo dos puntos.

salto:

...

if (expresion) goto salto;

P:335

Guía de sintaxis del lenguaje C 739

D.14. FUNCIONES

Las funciones son los bloques de construcción de programas C. Una función es una colección de declaraciones y

sentencias. Cada programa C tiene al menos una función: la función main. Esta es la función donde comienza la

ejecución de un programa C. La biblioteca ANSI C contiene gran cantidad de funciones estándar.

Formato

tipo_retorno nombre (tipo1 param1, tipo2 param2)

{

declaraciones_variables_locales

sentencia

sentencia

...

return expresion;

};

Llamada a funciones

nombre (arg1, arg2,...)

Declaración de prototipos

tipo_retorno nombre (tipo1 param1, tipo2 param2,...);

EJEMPLO

float valor_absoluto (float x)

{

if (x < 0)

x = -x;

return (x);

}

...

resultado = valor_absoluto (-15.5);

D.14.1. Sentencia return

La sentencia return detiene la ejecución de la función actual y devuelve al control a la función llamadora. La sintaxis es:

return expresion

en donde el valor de expresión se devuelve como valor de la función.

#include <stdio.h>

int main()

{

printf ("Sierra de Cazorla y \

");

printf ("Sierra Magina \

");

printf ("son dos hermosas sierras andaluzas \

");

return 0;

}

P:336

740 Fundamentos de programación

D.14.2. Prototipos de funciones

En ANSI C se debe declarar una función antes de utilizarla. La declaración de la función indica al compilador el tipo

de valor que devuelve la función y el número y tipos de argumentos que acepta.

El prototipo de una función es el nombre de la función, la lista de sus argumentos y el tipo de dato que devuelve.

int SeleccionarMenu(void);

double Area(int x, int y);

void salir(int estado);

D.14.3. El tipo void

Si una función no devuelve nada, ni acepta ningún parámetro, ANSI C ha incorporado el tipo de dato void, que es útil

para declarar funciones que no devuelven nada y para describir punteros que pueden apuntar a cualquier tipo de dato.

void exit (int estado);

void CuentaArriba (void);

void CuentaAbajo (void);

Errores típicos de funciones

1. Ningún retorno de valor. La función carece de una sentencia return. Si una función termina sin ejecutar

return, devuelve un valor impredecible, que puede producir errores serios.

2. Retornos omitidos. Si hay funciones que tienen sentencias if, hay que asegurarse de que existe una sentencia

return por cada camino de salida posible.

3. Ausencia de prototipos. Las funciones que carecen de prototipos se consideran devuelven int, incluso aunque

estén definidas para devolver valores de otro tipo. Como regla general, declarar prototipos para todas las funciones.

4. Efectos laterales. Este problema se produce normalmente por una función que cambia el valor de una o más

variables globales.

D.14.4. Detener un programa con exit

Un medio para terminar un programa sin mensajes de error en el compilador es utilizar la sentencia

return valor;

donde valor es cualquier expresión entera.

Otro medio para detener un programa es llamar a exit. La ejecución de la sentencia

exit(0);

termina el programa inmediatamente y cierra todos los archivos abiertos.

D.15. ESTRUCTURAS DE DATOS

Los tipos complejos o estructuras de datos en C son: arrays, estructuras, uniones, cadenas y campos de bits.

D.15.1. Arrays

Todos los arrays en C comienzan con el índice [0].

int notas[25]; /* declara array de 25 elementos*/

float lista[10][25]; /* declara array de 10 por 25 elementos */

P:337

Guía de sintaxis del lenguaje C 741

El número de elementos de un array se puede determinar dividiendo el tamaño del array completo por el tamaño

de uno de los elementos

noelementos = sizeof(lista) /sizeof (lista[0]);

Un array estático se puede iniciar cuando se declara con:

static char nombre[ ] = "Sierra Magina";

Un array auto se puede iniciar sólo con una expresión constante en ANSI C.

int listamenor [2][2] = {{25, 4}, {100, 75}};

int digitos[ ] = {0,1, 2, 3, 4, 5, 6, 7, 8, 9 };

Un array de punteros a caracteres se puede iniciar con:

char*colores[] = {"verde", "rojo", "amarillo", "rosa", "azul"};

Arrays unidimensionales

Los arrays se pueden definir para contener cualquier tipo de dato básico o cualquier tipo derivado. La declaración de

un array tiene el siguiente formato básico:

tipo nombre [n] = {valor_inicial, valor_inicial,...}

expresión constante

número de elementos del array

EJEMPLOS

char hoy [] = "Domingo"; se inicializa a: 'D', 'o', 'm', 'i', 'n', 'g', 'o', '\\0'

char hoy [7] = "Domingo"; se inicializa a: 'D', 'o', 'm', 'i', 'n', 'g', 'o'.

Arrays multidimensionales

El formato general para declarar un array multidimensional es:

tipo nombre [d1] [d2]...[dn] = lista_inicialización

expresión constante

EJEMPLO

int tres_d [5][2][20]; define un array de 3 dimensiones tres_d que contiene 200 enteros.

tres_d [4][0][15]=100; se almacena 100 en el elemento citado de tres_d.

Declaración de un array de dos dimensiones de cuatro filas y tres columnas

int matriz [4][3] ={

{1,2, 3}, primera fila: 1, 2 y 3.

{ 4, 5, 6}, segunda fila: 4, 5 y 6.

P:338

742 Fundamentos de programación

{ 7, 8, 9}, tercera fila: 7, 8 y 9.

{0,0,0} }; cuarta fila: 0, 0, 0.

La declaración anterior es equivalente a:

int matriz [4][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

Por último, la declaración

int matriz [4][3] ={ { 1} primer elemento de la primera fila a 1.

{ 4} segundo elemento de la primera fila a 4.

{ 7}}; tercer elemento de la tercera fila a 7.

resto elementos a cero.

D.16. CADENAS

Las cadenas son simplemente arrays de caracteres. Las cadenas se terminan siempre con un valor nulo ('\\0')

char cadena[80]; /* declara una cadena de 80 caracteres */

char mensaje[] = "Carchelejo está en Sierra Mágina";

char frutas[] [10] = {"naranja", "platano", "manzana" };

Concatenación de cadenas

El preprocesador concatena automáticamente constantes de cadenas de caracteres adyacentes. Las cadenas deben

estar separadas por cero o más caracteres o espacios en blanco.

"un" "carácter" "cadena"

equivale a

"uncaráctercadena"

D.17. ESTRUCTURAS

Las estructuras son colecciones de datos, normalmente de tipos diferentes, que actúan como un todo. Pueden contener tipos de datos simples (carácter, float, array y enumerado) o compuestos (estructuras, arrays o uniones).

Formato general

struct nombre

{

declaración_miembro

} lista_variables

declaración_miembro especificación de tipo seguida por una lista de uno o más nombres de miembros.

EJEMPLOS

1. struct fecha

{

int mes;

P:339

Guía de sintaxis del lenguaje C 743

int dia;

int anyo;

};

struct fecha hoy;

struct fecha fecha_compra;

struct fecha hoy, fecha_compra;

hoy.dia = 21;

hoy.anyo = 2000;

if (hoy.mes == 12)

mes_siguiente = 1;

2. struct coordenada {

int x;

int y;

};

struct signatura {

char titulo [40];

char autor [30];

int paginas;

int anyopubli;

};

Una variable de tipo estructura se puede declarar así:

struct coordenada punto;

struct signatura librosinfantiles;

Para asignar valores a los miembros de una estructura se utiliza la notación punto (.).

punto.x = 12;

punto.y = 15;

Existe otro modo de declarar estructuras y variables estructura.

struct coordenada {

int x;

int y;

} punto;

Para evitar tener que escribir struct cada vez que se declara la estructura, se puede usar typedef en la declaración:

typedef struct coordenada {

int x;

int y;

}Coordenadas;

y a continuación se puede declarar la variable punto:

Coordenadas punto;

y utilizar la notación punto (.)

punto.x = 12

punto.y = 15;

P:340

744 Fundamentos de programación

D.18. UNIONES

Las uniones son casi idénticas a las estructuras en su sintaxis. Las uniones proporcionan un medio de almacenar más

de un tipo en una posición de memoria. Se define un tipo unión así:

union tipodemo {

short x;

long l;

float f;

};

y se declara una variable con

union tipodemo demo;

y se puede utilizar

demo.x = 345;

o bien

demo.y = 324567;

La unión anterior se puede iniciar así:

union tipodemo { short x; long l; float f; } = {75};

D.19. CAMPOS DE BITS

Los campos de bits se utilizan con frecuencia para poner enteros en espacios más pequeños de los que el compilador

pueda normalmente utilizar y son, por consiguiente, dependientes de la implementación. Una estructura de campos

de bits especifica el número de bits que cada miembro ocupa. Una declaración de una estructura persona

struct persona {

unsigned edad; /* 0 .. 99 */

unsigned sexo; /* 0 = varon, 1 = hembra */

unsigned hijos; /* 0 .. 15 */

};

y de una estructura de campos de bits

struct persona {

unsigned edad: 7; /* 0..1127 */

unsigned sexo: 1; /0 = varon, 1 = hembra */

unsigned hijos: 4; /* 0..15 */

}

D.20. PUNTEROS (Apuntadores)

El puntero es un tipo de dato especial que contiene la dirección de otra variable. Un puntero se declara utilizando

el asterisco (*) delante de un nombre de variable.

float *longitudOnda; /* puntero a datos float */

char *indice; /* puntero a datos char */

int *p; /* puntero a datos int */

P:341

Guía de sintaxis del lenguaje C 745

posición del puntero posición de la variable

Dirección 500

Variable puntero Dato direccionado por puntero

Figura D.2. Un puntero es una variable que contiene una dirección.

D.20.1. Declaración e indirección de punteros

int m; /* una variable entera un */

int *p /* un puntero p apunta a un valor entero */

p = &m; /* se asigna a p la dirección de la variable m */

El operador de indirección (*) se utiliza para acceder al valor de la dirección contenida en un puntero.

float *longitudOnda;

longitudOnda = &cable1;

*longitudOnda = 40.5;

D.20.2. Punteros nulos y void

Un puntero nulo apunta a ninguna parte específica; es decir, no direcciona a ningún dato válido en memoria:

fp = NULL; /* asigna Nulo a fp */

fp = 0; /* asigna Nulo a fp */

if (fp != NULL) /* el programa verifica si el puntero es válido */

Al igual que una función void que no devuelve ningún valor, un puntero void apunta a un tipo de dato no especificado

void *noapuntafijo;

D.20.3. Punteros a valores

Para utilizar un puntero que apunte a un valor se utiliza el operador de indirección (*) delante de la variable puntero

double *total= &x;

*total = 34750.75;

El operador de dirección (&) asigna el puntero a la dirección de la variable

double *total;

double ctotal;

total = &ctotal;

D.20.4. Punteros y arrays

Si un programa declara

float *punterolista;

float listafloat [50];

P:342

746 Fundamentos de programación

entonces a punterolista se puede asignar la dirección del principio de listafloat (el primer elemento).

punterolista = listafloat;

o bien

punterolista = &listafloat [0];

listafloat[4] es lo mismo que *(listafloat + 4)

listafloat[2] es lo mismo que *(listafloat + 2)

D.20.5. Punteros a punteros

Los punteros pueden apuntar a otros punteros (doble indirección)

char **lista; /* se declara un puntero a otro puntero char */

equivale a

char *lista[];

y

char ***ptr /* un puntero a un puntero a otro puntero char */

D.20.6. Punteros a funciones

float *ptr; /* ptr apunta a un valor float */

float *mifun(void); /* la función mifunc devuelve un puntero a un valor float */

La declaración

int (*ptrafuncion)(void);

crea un puntero a una función que devuelve un entero, y la declaración

float (*ptrafuncion1)(int x, int y);

declara ptrafuncion1 como un puntero a una función que devuelve un valor float y requiere dos argumentos

enteros.

D.21. PREPROCESADOR DE C

El preprocesador de C es un conjunto de sentencias específicas, denominadas directivas, que se ejecutan al comenzar

el proceso de compilación.

Una directiva comienza con el símbolo # como primer carácter, que indica al preprocesador que ha de ejecutarse

una acción específica.

D.21.1. Directiva #define

Se utiliza para asociar identificadores con una secuencia de caracteres, éstos pueden ser una palabra reservada, una

constante, una sentencia o una expresión. En el caso de que el identificador represente sentencias o expresiones se

denomina macro.

Sintaxis: #define <identificador>[(parámetro)] <texto>

P:343

Guía de sintaxis del lenguaje C 747

EJEMPLOS

1. #define PI 3.141592

2. #define cuadrado(x) x*x

3. #define area_circulo(r) PI*cuadrado(r)

1. Se declara la constante PI. Cualquier aparición de PI en el programa será sustituida por el valor asociado:

3.141592.

2. Cuando cuadrado(x) aparece en una línea del programa, el preprocesador la sustituye por x*x.

3. La tercera directiva declara una operación matemática, una fórmula, que incluye las macros cuadrado(x) y

PI. Calcula el área de un círculo.

OTROS EJEMPLOS

#define LONGMAX 81

#define SUMA(x,y) (x)+(y)

El preprocesador sustituirá en el archivo fuente que contiene a estas dos macros por la constante 81 y la expresión

(x)+(y), respectivamente. Así:

float vector[LONGMAX]; define un vector de 81 elementos.

printf("%d",SUMA(5,7)); escribe la suma de 5+7.

Nota

Al ser una directiva del preprocesador no se pone al final ; (punto y coma).

D.21.2. Directiva #error

Esta directiva está asociada a la compilación condicional. En caso de que se cumpla una condición se escribe un

mensaje.

Sintaxis #error texto

El texto se escribe como un mensaje de error por el preprocesador y termina la compilación.

D.21.2. Compilación condicional

La compilación condicional permite que ciertas secciones de código sean compiladas dependiendo de las condiciones

señaladas en el código. Por ejemplo, puede desearse que el código no se compile si se utiliza un modelo de memoria

específico o si un valor no está definido. Las directivas para la compilación condicional tienen la forma de sentencias

if; son:

#if, #elif, #ifndef, #ifdef, #endif.

D.21.2.1. Directiva #if, #elif, #endif

Permite seleccionar código fuente para ser compilado. Se puede hacer selección simple o selección múltiple.

Formato 1 #if expresión_constante

. . .

#endif

P:344

748 Fundamentos de programación

Se evalúa el valor de la expresión_constante. Si el resultado es distinto de cero, se procesan todas las líneas

de programa hasta la directiva #endif; en caso contrario se saltan automáticamente y no se procesan por el compilador.

Formato 2 #if expresión_constante1

. . .

#elif expresión_constante2

. . .

#elif expresión_constanteN

. . .

#else

. . .

#endif

Se evalúa el valor de la expresión_constante1. Si el resultado es distinto de cero, se procesan todas las líneas

de programa hasta la directiva #elif. En caso contrario se evalúa la expresión_constante2, si es distinta de cero,

se procesan todas las líneas hasta la siguiente #elif o #endif. En caso contrario se sigue evaluando la siguiente

expresión, hasta que una sea distinta de cero, o bien, procesarse las líneas incluidas después de #else.

EJEMPLO

#if VGA

puts("Se está utilizando tarjeta VGA.");

#else

puts("hardware de gráficos desconocido.");

#endif

Se escribe "Se está utilizando tarjeta VGA." si VGA es distinto de cero; en caso contrario se escribe

la segunda frase.

D.21.2.2. Directiva #ifdef

Permite seleccionar código fuente según esté definida una macro (#define)

Formato #ifdef identificador

. . .

#endif

Si identificador está definido previamente con #define identificador entonces se procesan todas las líneas

de programa hasta #endif; en caso contrario no se compilan esas líneas. Al igual que la directiva #if, las directivas

#elif y #else se pueden utilizar conjuntamente con #ifdef.

D.21.2.3. Directiva #ifndef

Ahora la selección de código fuente se produce si no está definida una macro.

Formato #ifndef identificador

. . .

#endif

Si identificador no está definido previamente con #define identificador entonces se procesan todas las líneas

de programa hasta #endif; en caso contrario no se compilan esas líneas. Al igual que la directiva #if, las directivas

#elif y #else se pueden utilizar conjuntamente con #ifndef.

P:345

Guía de sintaxis del lenguaje C 749

EJEMPLO

#ifndef _PAREJA

#define _PAREJA

struct pareja {

. . .

}

. . .

#endif

En el ejemplo, si no está definida la macro _PAREJA se define y se procesa el código fuente que está escrito

hasta #endif. Se evita que se procese el código más de una vez.

D.21.3. Directiva #include

Permite añadir código fuente escrito en un archivo al archivo que actualmente se está escribiendo.

Formato 1 #include “nombre_archivo”.

El archivo nombre_archivo se incluye en el módulo fuente. El preprocesador busca en el directorio o directorios especificado en el path del archivo. Normalmente se busca en el mismo directorio que contiene al archivo fuente. Una vez que se encuentra se incluye el contenido del

archivo en el programa, en el punto preciso que aparece la directiva #include.

Formato 2 #include <nombre_archivo>.

El preprocesador busca el archivo especificado sólo en el directorio establecido para contener

los archivos de inclusión.

D.21.4. Directiva #line

Formato #line constante “nombre_archivo”.

La directiva produce que el compilador trate las líneas posteriores del programa como si el nombre del archivo fuente fuera “nombre_archivo” y como si el número de línea de todas las líneas

posteriores comenzara por constante. Si “nombre_archivo” no se especifica, el nombre de archivo especificado por la última directiva #line, o el nombre del archivo fuente se utiliza (si ningún archivo se especificó previamente).

La directiva #line se utiliza principalmente para controlar el nombre del archivo y el número de línea que se

visualiza siempre que se emite un mensaje de error por el compilador.

D.21.5. Directiva #pragma

Formato #pragma nombre_directiva.

Con esta directiva se declaran directivas que usa el compilador de C. Si al compilarse el programa con otro compilador de C, éste no reconoce la directiva, se ignora.

D.21.6. Directiva #undef

Formato #undef identificador

El identificador especificado es el de una macro definida con #define, con la directiva #undef

el identificador se convierte en no definido por el preprocesador. Las directivas posteriores

#ifdef, #ifndef se comportarán como si el identificador nunca estuviera definido.

P:346

750 Fundamentos de programación

D.21.7. Directiva #

Formato #

Es la directiva nula; el preprocesador ignora la línea que contiene la directiva #.

D.21.8. Identificadores predefinidos

Los identificadores siguientes están definidos para el preprocesador:

Identificador Significado

__LINE__ Número de línea actual que se compila.

__FILE__ Nombre del archivo actual que se compila.

__DATE__ Fecha de inicio de la compilación del archivo actual.

__TIME__ Hora de inicio de la compilación del archivo, en el formato "hh:mm:ss".

__STDC__ Constante definida como 1 si el compilador sigue el estándar ANSI C.

P:347

Bibliografía y recursos

de programación

1. BIBLIOGRAFÍA

FUNDAMENTOS Y METODOLOGÍA DE LA PROGRAMACIÓN

BROOKSHEAR, J. Glenn (2005). Computer Science: An Overview. Eigth Edition. Boston: Pearson/Addison Wesley.

CLEMENTE, P. J. y GONZÁLEZ, J. (2005). Metodología de la Programación. Enfoque práctico. Cáceres: UEX.

CRIADO, M.ª Asunción (2005). Programación en lenguajes estructurados. Madrid: Rama.

DAHL, O., DIJKSTRA, E. y HOARE, C. A. R. (1972). Structured Programming. London. England: Academic

Press.

FARREL, Joyce (2004). Programming Logic and Design. Boston, Massachussetts: Thomson.

FOROUZAN, Behrouz A. (2003). Introducción a la Ciencia de la Computación. De la manipulación de datos a la

teoría de la computación. México DF: Thomson.

JIMÉNEZ, F., MARTÍNEZ, G., MATEO, A., PAREDES, S., PÉREZ, F., SÁNCHEZ, G. (2006). Metodología y Tecnología de la programación. Murcia: Universidad de Murcia.

JOYANES, Luis (1986). Metodología de la programación. Madrid: McGraw-Hill.

JOYANES, Luis (2003). Fundamentos de programación. 3.ª edición. Madrid: McGraw-Hill.

JOYANES, L., RODRÍGUEZ, L. y FERNÁNDEZ, M. (2003). Fundamentos de programación. Libro de problemas.

2.ª edición. Madrid: McGraw-Hill.

KNUTH, D. E. (1969). The art of Computer Programming, Vol. 1. Fundamental Algorithms, Vol. 2. Sorting and

Searching, 1972. Addison Wesley.

KNUTH, D. E. (1973). The art of Computer Programming, Vol. 2. Sorting and Searching. Addison Wesley.

KNUTH, D. E. (1974). Structured programming with go-to statements. ACM Computing Surveys. Vol. 6, 4, pp. 261-301.

KNUTH, D. E. (1997). The Art of Computer Programming, Vols. 1, 2 y 3. Reading, Massachussetts: Addison-Wesley.

MARTÍNEZ, F. A. y MARTÍN, G. (2003). Introducción a la programación estructurada en C. Valencia: Universitat

de Valencia.

PERRY, Greg. (2001). Absolute Beginner's Guide to Programming. Second edition. Indianapolis: Que.

PRIETO, A. y PRIETO, B. (2005). Conceptos de Informática. Madrid: McGraw-Hill (colección Schaum).

SEBESTA, Robert. (2002). Concepts of Programming Languages. Boston: Addison-Wesley.

ALGORTIMOS Y ESTRUCTURAS DE DATOS

BERGIN, T. J. JR. y GIBSON, R. G. JR. (ed.) (1996). History of Programming Languages II. Addison-Wesley.

BRASSARD, G. y BRATLEY. P. (1997). Fundamentos de Algoritmia. Madrid: Prentice Hall. (Este libro fue traducido al español por los profesores María Luisa Díez y Luis Joyanes de la Facultad de Informática de la Universidad Pontificia de Salamanca.)

CARRANO, F. M. y SAVITH, Walter (2003). Data Structures and Abstractions with Java. Upper Saddle River, NJ:

Prentice-Hall.

DASGUPTA, S., PAPADIMITRIOU, C. y VAZIRANI, U. (2008). Algorithms. NewYork: McGraw-Hill.

HERNÁNDEZ, Z. J., RODRÍGUEZ, J. C., GONZÁLEZ, J. D., DÍAZ, M., PÉREZ, J. R., RODRÍGUEZ, G. (2005).

Fundamentos de Estructuras de Datos. Soluciones en Ada, Java y C++. Madrid: Thomson.

P:348

752 Fundamentos de programación

GARCÍA MOLINA, J. J., MONTOYA, F. J., FERNÁNDEZ, J. L. y MAJADO, M. J. (2005). Una introducción a la

programación: Un enfoque algorítmico. Madrid: Thomson.

GARRIDO, A. y FERNÁNDEZ, J. (2006). Abstracción y Estructuras de datos en C++. Madrid: Ediciones Delta.

GIUSTI, Armando E. de (2001). Algoritmos, datos y programas. Buenos Aires: Prentice-Hall.

HUBBARD, John (2001). Data Structures with Java. New York: McGraw-Hill.

JAIME SISA, A. (2002). Estructuras de datos y Algoritmos con énfasis en programación orientada a objetos. Bogotá: Prentice-Hall.

JOYANES, L. (1987): Introducción a la teoría de ficheros (archivos). UPS.

JOYANES L. y ZAHONERO I. (1998). Estructura de datos. Madrid: McGraw-Hill.

JOYANES L. y ZAHONERO I. (2004). Algoritmos y estructura de datos. Madrid: McGraw-Hill.

JOYANES, L. y ZAHONERO, I. (2004). Algoritmos y estructuras de datos: Una perspectiva en C. Madrid: McGrawHill.

JOYANES, L. y ZAHONERO, I. (2007). Estructuras de datos en C++. Madrid: McGraw-Hill.

JOYANES, L y ZAHONERO, I. (2007). Estructuras de datos en Java. Madrid: McGraw-Hill.

JOYANES, L., FERNÁNDEZ, M., ZAHONERO, I. y SÁNCHEZ, L. (2006). Estructuras de datos en C. Madrid:

McGraw-Hill. Colección Schaum.

JOYANES, L., ZAHONERO, I. y SÁNCHEZ, L. (2006). Estructuras de datos en C++. Madrid: McGraw-Hill. Colección Schaum.

LAFORE, Robert. Data Structures & Algorithms in Java. Indianapolis, Indiana: SAMS.

KOFFMAM, Elliot B. y WOLFGANG, Paul. A. T. (2006). Objetcs, Abstraction, Data Structures and Design. Using

C++. Nueva York; John Wiley.

KRUSE, Robert L. (1987). Data Structures and Program Design. 2.ª edición. PHI.

MARTÍ, N., ORTEGA, Y. y VERDEJO, J. A. (2003). Estructuras de datos y métodos algorítmicos. Madrid: Pearson/

Prentice-Hall.

NYHOFF, Larry R. (2005). TADs, Estructuras de datos y resolución de problemas con C++. Madrid: Pearson/Prentice-Hall.

PEÑA, Ricardo (2006). De Euclides a Java. Historia de los Algoritmos y de los lenguajes de programación. Madrid:

Nivola.

STANDISH, Thomas (1995). Data Structures, Algorithms & Software Principles in C. Reading, Massachusett: Addison-Wesley.

WEISS, Mark Allen (2006). Data Structures and Algorithm Analysis in C++. Boston: Pearson/Addison-Wesley.

WEISS, Mark Allen (2007). Data Structures and Algorithm Analysis in Java. Boston: Pearson/Addison-Wesley.

WIRTH, N. (1986). Algorithms and Data Structures. Englewood Cliffs, NJ: Prentice Hall.

ZIVIANI, Nivio (2007). Diseño de Algoritmos con implementaciones en Pascal y C. Madrid: Thomson.

PROGRAMACIÓN ORIENTADA A OBJETOS

BARNES, David J. y KÖLLING, Michael (2006). Objects First with Java. Harlow, England: Pearson.

CACHERO, C., PONCE DE LEÓN, P. J. y SAQUETE, E. (2006). Introducción a la Programación Orientada a Objetos. Alicante: Universidad de Alicante.

DURAN, F., GUTIÉRREZ, F. y PIMENTEL E. (2007). Programación orientada a objetos con Java. Madrid: Thomson/Paraninfo.

GILBERT, Stephen y McCARTHY, Bill (1998). Object-Oriented Design in Java. Corte Madera, CA: The Waite

Group.

JOYANES, Luis (2006). Programación Orientada a Objetos. 2.ª edición. Madrid: McGraw-Hill.

JOYNER, Ian (1999). Objects Unencapsulated: Java, Eiffel and C++. Upper Saddle Rider, New Jersey: PrenticeHall.

LAFORE, Robert (2002). Object-Oriented Programming in C++. Fourth Edition. Indianapolis, Indiana: SAMS.

INGENIERÍA DE SOFTWARE TRADICIONAL Y ORIENTADA A OBJETOS

BOOCH, Grady (1994). Object-Oriented Analysis and Design with applications. The Benajming/Cummings Publishing Company. Este libro fue traducido al español por los profesores: Cueva, de la Universidad de Oviedo y

Joyanes, de la Universidad Pontificia de Salamanca.

P:349

Bibliografía y recursos de programación 753

BOOCH, Grady et al. (2007). Object-Oriented Analysis and Design with applications. Third edition. Upper Saddle

River, NJ: Addison-Wesley.

BROOKS, F. (1975). The Mythical Man-Month. Reading, MA: Addison-Wesley.

HAMLET, Dick y MAYBEE, Joe (2004). The Engineering of Software. Boston: Addison-Wesley.

LAUDON, K. y LAUDON, J. (2003). Essentials of Management Information Systems. Fifth Edition. Pearson/Prentice-Hall.

MEYER, B. (1995). Object Succes; A Manager’s Guide to Object-Oriented Technology and Its Impact on the Corporation. Upper Saddle River, NJ: Prentice-Hall.

PISCITELLI, Mario (2001). Ingeniería de Software. Madrid: Rama.

PRESSMAN, Roger (2005). Ingeniería de Software. Un enfoque práctico. 6.ª edición. México DF: McGraw-Hill.

SOMMERVILLE, I. (1989). Software Engineering. Third Edition. Wokingham, England: Addison-Wesley.

UML

ALBIR, Sinan Si (2003). Learning UML. Sebastopol, CA (USA): O’Reilly.

AMBLER, Scott William (2004). The Object Primer: Agile Model Driven Development with UML 2. Cambridge

University Press.

BENNET, S., McROBB, S. y FARMER, R. (2006). Object-Oriented Systems Analysis and Design Using UML. London: McGraw-Hill.

BLAHA, M., y RUMBAUGH, J. (2005). Object-Oriented Modeling and Design with UML. Second Edition. Upper

Saddle River, NJ: Prentice Hall.

BOOCH, G., RUMBAUGH, J. y JACOBSON, I. (2006). El lenguaje unificado de Modelado. 2.ª edición. Madrid:

Pearson/Addison-Wesley. Obra traducida al español por los profesores García Molina y Sáez Martínez, de la

Universidad de Murcia.

CAMPDERRICH, B. (2003). Ingeniería del software. Barcelona: Editorial UOC.

CHONOLES, Michael Jesse y SCHARDT, James A. (2003). UML 2 for Dummies. Wiley Publishing.

COAD, Peter; LEFEBVRE, Eric; LUCA, Jeff De (1999). Java Modeling In Color With UML: Enterprise Components

and Process. Prentice-Hall.

DEBRAUWER, L. y VAN DER HEYDE, F. (2005). UML 2. Iniciación, ejemplos y ejercicios corregidos. Barcelona:

Ediciones ENI.

DENNIS, A., WISON, B. H. y TEGARDEN, D. (2005). Systems Analysis and Design with UML. Versión 2.0. Second

edition. John Wiley.

ERIKSSON, H. E., PENKER, M., LYONS, B. y FADO, D. (2004). UML 2 Toolkit. Indianapolis, Indiana: John

Wiley.

FOWLER, M. (2003). UML Distilled: A Brief Guide to the Standard Object Modeling Language. Third Edition.

Boston, MA: Addison-Wesley.

GÓMEZ, C., MAYOL, E. M., OLIVÉ, A. y TENIENTE, E. (2003). Diseño de sistemas de software en UML. Barcelona. Editions UPC.

HENDERSON SELLERS, Brian (2006). About UML profiles. Springer Verlag. MODELS’2006. Conference, Genova.

JACOBSON, Ivar, BOOCH, Grady y RUMBAUGH, James (1998). The Unified Software Development Process.

Addison Wesley Longman.

LEE, Richard C. y TEPFENAHART, William M. (2001). UML and C++. A Practical Guide To Object-Oriented

Development. Second edition. Upper Saddle River, New Jersey: Prentice-Hall.

MARTIN, R. C. (2003). UML for Java Programmer’s. Upper Saddle River, NJ: Pearson/Prentice-Hall.

MULLER, Pierre-Alain (1997). Modelado de objetos con UML. Barcelona: Eyrolles/Gestión 2000.

MILLES, R. y HAMILTON, K. (2006). Learning UML 2.0. Sebastopol, CA (USA): O’Reilly.

MULLER, Pierre-Alain (1997). Instant UML. Birmighan, UK: Wrox.

OBJECT MANAGEMENT GROUP (2004). UML Superstructure Specification, v2.0. www.uml.org.

PERDITA, Stevens y ROOLEY, Rob (2007). Utilización de UML en Ingeniería del Software con objetos y componentes. 2.ª edición. Madrid: Pearson/Addison-Wesley. Esta obra ha sido traducida por los profesores Marta Fernández y Ruben González, de la Facultad de Informática de la Universidad Pontificia de Salamanca, bajo la dirección técnica del profesor Luis Joyanes.

R.S. PRESSMAN & ASSOCIATES, INC. Sitio Web del prestigioso experto en Ingeniería de Software, Roger PRESSMAN (véase referencias de INGENIERÍA DE SOFTWARE). http://www.rspa.com/reflib/UMLRelatedMaterials.html.

P:350

754 Fundamentos de programación

RUMBAUGH, J., JACOBSON, I. y BOOCH, G. (2005). The Unified Modeling Language Reference Manual. Second

Edition. Boston, MA: Addison-Wesley. (Esta obra ha sido traducida al español por un equipo de profesores de la

Facultad de Informática de la Universidad Pontificia de Salamanca, Hector Castán, Oscar Sanjuan y Mariano,

dirigidos por el profesor Luis Joyanes.

SCHACH, S. (2004). Introduction to Object-Orientd analysis and Design with UML. New York: McGraw-Hill.

SCHMULLER, J. (2004). Teach Yoursef UML in 24 hours. Third edition, Indianapolis: SAMS.

STEVENS, P. y POOLEY, R. (2007). Using UML. Software Engineering with Objects and Components. Harlow:

Gran Bretaña: Addison-Wesley. Libro traducido al español por los profesores Marta Fernández y Ruben González,

de la Universidad Pontificia de Salamanca, dirigidos por el autor de esta obra.

PAGE-JONES, M. (2000). Fundamentals of Object.Oriented Design in UML. Reading, Massachussets: AddisonWesley.

C

ALERA, M. A. y SANZ, A. M. (2005). Manual imprescindible de C/C++. Madrid: Anaya.

ANTONAKOS, J. L. y MANSFFIELD, K. C. (1997). Programación estructurada en C. Madrid: Prentice-Hall.

CLEMENTE, P. J. y GONZÁLEZ, J. (2005). Metodología de la programación: enfoque práctico. Cáceres: Universidad de Extremadura.

DELANNUY, S. y DELANNUY, C. (2001). El libro de C como primer lenguaje. Barcelona: Eyrolles/Ediciones

Gestión 2000.

JOYANES, L., CASTILLO, A., SÁNCHEZ, L. y ZAHONERO, I. (2005). C: Algoritmos, programación y estructuras

de datos. Madrid: McGraw-Hill.

JOYANES, Luis y ZAHONERO, Ignacio (2004). Programación en C. 2.ª edición. Madrid: McGraw-Hill.

KELEY, A. y POOL, I. (1984). A book on C. An Introduction to programming in C. Merlo Park, CA: The Benajmin/

Cumming.

KERNIGHAN, B, W. y RITCHIE, D. M. (1991). El lenguaje de programación C. 2.ª edición, Mexico DF: PrenticeHall.

HORTON, Ivor. (2004). Beginning C. Third edition. Berkeley: Appres.

MUÑOZ, J. D. y PALACIOS, R. Fundamentos de programación utilizando el lenguaje C. Madrid: UPCO, 2006.

PLAUGER, P. J. (1994). The Standard C Library. Upper Saddle River, NJ: Prentice-Hall.

PLAUGER, P. J. (1995). The Drafts Standard C++ Library. Upper Saddle River, NJ: Prentice-Hall.

PLAUGER, P. J. (1996). Standard C: A Reference. Upper Saddle River, NJ: Prentice-Hall.

ROBERT, E. S. (1995). The Art and Science of C. Addison-Wesley

RODRÍGUEZ, J. M. y GALINDO, J. (2006). Aprendiendo C. Cádiz: Universidad de Cádiz.

VEGA, M. A. y SÁNCHEZ, J. M. (2003). Fundamentos de programación en C. Cáceres: Universidad de Extremadura.

C++

ABURRUZAGA, G., MEDINA, I., PALOMO, F. (2001). Fundamentos de C++. Cádiz: Universidad de Cádiz.

BALAGURUSAMY (2006). Object-Oriented Programming with C++. Third Edition. New Delhi: McGraw-Hill.

BROSTON, Gary I. (1999). C++ for Engineers and Scientist. Pacific Grove: Brook/Cole. Thomson.

CACHERO, C., PONCE DE LEÓN, R. J., SAQUETE, E. (2006). Introducción a la programación orientada a objetos. Alicante: Universidad de Alicante.

DALE, Nell y WEEMS, Chip (2007). Programación y resolución de problemas con C++. 4.ª edición. México DF:

McGraw-Hill.

DAVIS, Stephen Randy (2001). C++ para Dummies. 4.ª edición. Panamá: ST Editorial.

DEITEL, H. M. y DEITEL, P. J. (2004). Cómo programar en C/C++ y Java. Cuarta edición. México DF: NJ. Prentice-Hall.

DEITEL, H. M., DEITEL, P. J., CHOFFNES, D. R. y KELSEY, C. L. (2005). Simply C++. Upper Saddle River, NJ:

Pearson/Prentice-Hall.

GARRIDO, Antonio (2006). Fundamentos de Programación en C++. Madrid: Delta Publicaciones.

GLASSBOROW, Francis (2006). You Can Program in C++. West Sussets: England, Wiley.

GUERIN, Brice-Arnaud (2005). Lenguaje C++. Barcelona: Editiciones Eni.

P:351

Bibliografía y recursos de programación 755

JOSUTIS, Nicolai M. (2003). Object-Oriented Programming in C++. Sussex, Gran Bretaña: Wiley.

JOYANES, Luis (2006). Programación en C++. 2.ª edición. Madrid: McGraw-Hill.

LAFORE, Robert (2002). Object-Oriented Programming in C++. Fourth edition. Indianapolis: SAMS.

LIBERTY, Jesse (1999). C++ from scratch. Indianapolis, Indiana: Que.

LIBERTY, Jesse y HORVATH, David B. (2005). Aprenda C++. Madrid: Anaya Multimedia.

LIANG Y., Daniel (2007). Introduction to Programming with C++. Comprehensive Version. Upper Saddle River, NJ:

Pearson/Prentice-Hall.

LISCHNER, Ray (2003). C++ in a Nutshell. Sebastopol, CA: O'Reilly.

LIPPMAN, S. B., LAJOIE, J. y MOO, B. E. (2005). C++ Primer. Fourth edition: Upper Saddle River, NJ: AddisonWesley.

MILEWSKY, Bartosz (2001). C++ in Action. Boston: Addison-Wesley.

OUALLINE, Steve (2003). Practical C++ Programming. 2nd edition. Sebastopol (USA): O’Reilly.

PRATA, Stephen (2005). C++ Primer Plus. Fifth edition. Indianapolis: Sams.

SAVITCH, Walter (2006a). Problem Soving with C++. 6th edition. Boston: Pearson/Addison-Wesley.

SAVITCH, Walter (2006b). Absolute C++. Second edition. Boston: Pearson/Addison-Wesley.

SOLTER, Nicholas A. y KLEPPER, Scott J. (2006). Professional C++. Indianapolis, IN: Wrox.

STEPHE, R. D., DIGGINS, C., TURKANIS, J. y COGSWELL, J. (2006). C++ Cookbok. Sebastopol, CA:

O’Reilly.

TONDO, Clovis L. y LEUNG, Bruce P. (1999). C++ Primer Answers Book. Reading. Massachusetts: Addsion-Wesley.

XHAFA, F., VÁZQUEZ, P. P., MARCO, J., MOLINERO, X. y MARTIN, A. (2006). Programación en C++. Madrid:

Thomson.

Java

BARNES, David J. (2000). Object-Oriented Programming with Java. An Introduction. Upper Saddle River, NJ: Prentice-Hall.

CADENHEAD, R. y LEMAY, L. (2007). Teach Yourself Java 6 in 21 Days. Indianapolis: Sams.

CHEW, Frederick F. (1998). The Java/C++. Cross-Reference Handbook. Upper Saddle River, NJ: Prentice-Hall.

DEITEL, H. M. y DEITEL, P. J. (2002). Java. How to Program. Fourth edition. Upper Saddle River, NJ: PrenticeHall.

GARCÍA-BERMEJO, José Rafael (2007). Java 6SE. Madrid: Pearson.

JOYANES, Luis y ZAHONERO, Ignacio (2002). Programación en Java 2. Madrid: McGraw-Hill.

MOLDES, F. Javier (2007). Java SE 6. Guía Práctica. Madrid: Anaya Multimedia.

REBELSKY, Samuel A. (2000). Experiments in Java. Reading Massachusetts: Addison Wesley.

WEISS, Mark Allen (1999). Data Structures & Algorithm Analysis in Java. Reading Massachusetts: Addison Wesley.

C#

BISCHOF, Brian (2002). The NET Languages: A Quicl Translation Guide. New York: Appres.

PASCAL y MODULA-2

CERRADA, J. A., COLLADO, M., GÓMEZ, S. y ESTIVÁRIZ, J. F. (2006). Fundamentos de programación con

Modula-2. Madrid: Editorial Centro de Estudios Ramón Areces.

GIUSTI, Armando E. de (2001). Algoritmos, datos y programas. Buenos Aires: Prentice-Hall.

JOYANES, L. (2006). Programación en Pascal, 4.ª edición. Madrid: McGraw-Hill.

P:352

756 Fundamentos de programación

C/C++ Users Journal.

www.cuj.com

Dr. Dobb's Journal.

www.ddj.com

Java Report.

www.javareport.com

Linux Magazine.

www.linux-mag.com

MSDN Magazine.

msdn.microsoft.com/msdnmag

PC Magazine.

www.ppcmag.com

PC Actual.

www.pc-actual.com

PC World.

www.pcworld.com

Sys Admin.

www.samag.com

Java Pro.

www.java-pro.com

JavaWorld (on-line).

www.javaworld.com

Software Development Magazine.

www.sdmagazine.com

UNIX Review.

www.review.com

Windows Developper's Journal.

www.wdj.com

Component Strategies.

www.componentmag.com

C++ Report.

www.creport.com

Journal Object Oriented Programming (JOOP).

www.joopmag.com

Microsoft Systems Journal.

www.msj.com/msjquery.html

Visual C++ Developer Journal.

www.vcdj.com/

PC World España.

www.idg.es/pcworld

Dr. Dobb's (en español).

www.mkm-pi.com

Java Developerl's Journal.

www.sys-con.com/java/

2. RECURSOS DE PROGRAMACIÓN EN INTERNET

Revistas de informática/computación de propósito general y/o con secciones

especializadas de programación y en lenguajes de programación C/C++/Java

Editoriales especializadas en programación (técnicas y lenguajes)

Addison-Wesley www.awprofessional.com

Anaya Multimedia www.AnayaMultimedia.es

Apress (Springer-Verlag) www.apress.com

Delta Ediciones

Macmillan

McGraw-Hill www.mcgraw-hill.com

McGraw-Hill España www.mcgraw-hill.es

McGraw-Hill/Osborne www.osborne.com

Microsoft Press mspress.microsoft.com/developer

O'Reilly www.oreilly.com

Paraninfo www.paraninfo.es

Pearson www.pearsoneducacion.com

Que www.que.com

Prentice Hall www.phptr.com

Rama www.ra-ma.com

Sams www.samspublishing.com

Thomson www.thomsonlearnign.com

Thomson Paraninfo www.thomsonparaninfo.com

Waite Group www.waitegroup.com

Wiley www.wiley.com

Wrox www.wrox.cm

P:353

Bibliografía y recursos de programación 757

Enlaces de utilidad

Enlace a páginas con recursos útiles, compresores de archivos, artículos, cursos de C++, biblioteca de lenguajes, foros, compiladores, etc.

Software shareware.

www.shareware.com

La web del programador.

www.lawebdelprogramador.com

Recursos de programación de Universidad de Málaga.

www.ieev.uma.es/fundinfo/enlaces/enlac.htm

Directorios de Google, Yahoo, Live, Ask, A9, Amazon

www.google.es/Top/World/Espa%C3%B1ol

Thefreecountrycom.

www.thefreecountry.com/compilers/cpp.shtml

Recursos importantes de C/C++

Biblioteca de C de la ACM (Universidad de Illinois).

www.acm.uiuc.edu/webmonkeys/bok/c_guide

Codeguru.com (sitio muy popular con excelentes recursos de programación).

www.codeguru.com/Cpp/Cpp/cpp_mfc/

DevX.

www.dev.com/cplus

Dinkuware (licencias, estándares, documentación…).

www.dinkuware.com/libraries_ref.html

www.dinkuware.com/refxc.htnml (Biblioteca C estándar)

Página de Dennis M. Ritchie.

www.cs.bell-labs.com/who/dmr/index.html

Lysator. Colección de artículos y libros relativos a C y ANSI C.

www.lysator.liu.se/c/

The Annotated C Standard.

www.lysator.liu.se/c/schildt.html

Revista Microsoft Systems Journal.

www.msj.com/msjquery.html

Página oficial de Microsoft sobre Visual C++.

msdn.microsoft.com/developer

Página oficial del fabricante Inprise/Borland.

www.borland.com

Programmer's Book List (excelente referencia de libros de C).

www.sunir.org/booklist

The Development of the C Language.

www.lysator.liu.se/c/

Recursos de programación y bibliotecas C/C++: Thefreeprogramming.

www.freeprogrammingresoureces.com/cpplib.html

Programaming C en Wikibooks.

en.wikibooks.org/wiki/Programming:C

Historia de C en la enciclopedia Wikipedia (10 páginas excelentes).

en.wikipedia.org/wiki/C_programming_language

P:354

758 Fundamentos de programación

Página web de Bjarne Stroustrup.

www.research.att.com/~bs/C++.html

Excelente página de orientación a objetos en español.

www.ctv.es/USERS/pagullo/cpp.htm

Manuales y bibliotecas de C y C++, compatibles con estándares y documentación.

www.dinkunware.com/libraries_ref.html

Recursos de C/C++ (excelente sitio de consulta y referencia).

www.programmersheavem.com

www.programmersheavem.com/zone3/cat353/index.htm (Bibliotecas C/C++)

www.programmersheavem.com/zone3/cat155/index.htm (Utilidades C/C++)

Sitio Web de la Universidad de California en San Diego (origen de C).

ccs.ucsd.edu/C

Página de Visual C++ de Microsoft.

www.microsoft.com/visualc/

Estandares de C

K&R (The C Programming Language, 1978).

ANSI C (Comité ANSI X3.159-1989, 1989).

ANSI C (adoptado por ISO como ISO/IEC 9899: 1990, 1990).

C99 (ISO 9899:1999).

www.ansi.org

www.comeaucomputing.com/techtalk/c99

InterNational Committe of Information Technology Support (Grupo de asesoramiento tecnológico de ANSI

para ISO/IEC Joint Technical Committee. Publicaciones y adquisiciones sobre C99).

www.incits.com

Biblioteca C estándar: ISO/IEC 9899:1990, Programming Languages-C.

Amendment 1:1995(E), C Integrity.

ISO/IEC 14882:1998(E) Programming Languages – C++.

Sitios web de C++

Existen cientos de miles de páginas web referidas a C++ (el día de la última consulta del término C++: en Google

233.000.000 páginas; en Yahoo, 55.100.000) por lo que hemos seleccionado algunas de las más significativas atendiendo a la notoriedad e importancia del sitio en base al autor, organización, centro de recursos, etc. y que consideramos serán de gran utilidad para el lector en su fase de aprendizaje y sobre todo en su fase profesional.

Página Web de Bjarne Stroustrup (creador de C++).

www.research.att.com/˜bs

Cplusplus.com.

www.cplusplus.com

Página con gran cantidad de datos relativos a C++: tutoriales, información de compiladores, forum, …

C++ FAQ Lite

//parashift.com/c++-faq-lite/index.html

Página muy importante de preguntas realizadas más frecuentemente.

Cprogramming.com.

www.cprogramming.com

Tutoriales, herramientas, recursos, etc.

Acerca de C/C++/C#.

//cplus.about.com

Parte de un sitio web más completo sobre numerosos temas.

P:355

Bibliografía y recursos de programación 759

Guías de estilo de C y C++.

www.chris-lott.org/resources/cstyle

Reglas y normas para buenos estilos de programación.

Estándares abiertos.

www.open-std.org

Comité de estándares de C++.

www.open-std.org/JTC1/SC22/WG21/

C++ Standard: ANSI Draft/ISO Working Papers

www.csi.csusb.edu/dick/c++std/

ANSI/ISO C++ Professional Programmer's Handbook. Que.

www-f9.ijs.si/~matevz/docs/C++/ansi_cpp_progr_handbook/index.htm

Sitios web de Java

Recursos Java

Revista Java Programing www.java-pro.com

Revista en línea JavaWorld www.javaworld.com

Revista Java Developer’s Journal www.sys-con.com/java

Revista Java Report www.javareport.com

Revista SunWorld www.sun.com/sunworldonline

Intelligence.com

(recursos de Java e información) www.intelligence.com/java/default.htm

Caffeine Connection http://www.online-magazine.con/cafeconn.htm

Recursos y productos software

Sun Mycrosystems, Inc. java.sun.com

Java Developer Conection de Sun java.sun.com/jdc/

IDE Sun One Studio www.sun.com/software/sundev/jde/index.html

Java products (SDK) Java.sun.com/products

Programmers Source www.progsource.com

Java Developers Sun www.java.sun.com/developer

The IBM Developers Java

Tehcnology Zone www-106.ibm.com/developerwprks/subscription/downloads

The IBM Developers Java Tehcnology www.ibm.com/developer/java

IBM WebSphere Studio www-306.ibm.com/software/awdtools/studiositedev

Java Class Libraries java.sun.com/products/jdk/1.1/docs/api

java.sun.com/products/jdk/1.1/docs/api/API_users_

guide_html

Java Development Kit (JDK) java.sun.com/docs

Sitio de Gamelan

(directorio official de Java) www.developer.com/java

JARS

(Java Applet Rating Service) www.jars.com

JBuilder www.borland.com/jbuilder

Productos

JDK y otros productos de Sun java.sun.com/products

Borland JBuilder www.borland.com/jbuilder

Imprise www.imprise.com

P:356

760 Fundamentos de programación

Visual Café Integrated Development

Environment cafe.symantec.com

Visual Age de IBM www.software.ibm.com/ad/vajava/

Tutoriales

Java Tutorial Site java.sun.com/docs/books/tutorial

Programmers Source www.progsource.com

FAQs

Java Toys www.nikos.com/javatoys/

Java Woman javawoman.com/index.html

Sun RMI y Objet Serialization FAQ java.sun.com/products/jdk/rmi/faq.html

Sun JDBC FAQ java.sun.com/products/jdbc/faq.html

Development Exchange Java Zone www.devx.com/java

iBiblio www.ibiblio.org/javafaq

Applets Java

Sitio Sun java.sun.com

Sitio Sun de applets java.sun.com/applets/index.html

Java Developer Conection java.sun.com/jdc/

Java Applet Rating Service www.jars.con

Sitios web de interés

www.sun.com

java.sun.com/

www.hp.com/gsyinternet/hpjdk/

www.javaworld.com

www.gamelan.com/

www.sigs.com/jro/

www.ibiblio.org/javafaq

Libros sobre Java

Libros de referencia de Sun Java.sun.com/docs/books

www.awl.com/cseng/javaseries

Librerías técnicas

de Java Amazon ww.amazon.com

Organizaciones Internacionales de Computación

ACM.

www.acm.org

IEEE.

www.ieee.org

ACCU (Association of C and C++ Users).

www.accu.org/

ANSI (American National Standards Institute).

www.ansi.org

P:357

Bibliografía y recursos de programación 761

International Committe of Information Technology Suppor.

www.incits.com

Comité ISO/IEC JTC1/SC22/WG14-C.

//anubis.dkuug.dk/JTC1/SC22/WG14/

Comité encargado de la estandarización y seguimiento de C.

Comité ISO/IEC JTC1/SC22/WG21-C++.

anubis.dkuug.dk/jtc1/sc22/wg21/

Comité encargado de la estandarización y seguimiento de C++.

ISO (International Organization for Standardization).

www.iso.ch/

Organización de aprobación de estándares de ámbito internacional (entre ellos de C/C++).

3. COMPILADORES

Compiladores y lenguajes de programación gratuitos de diferentes lenguajes

[C/C++][COBOL][Delphi][Eiffel][Ensamblador(ASM)][FORTRAN][Java][Pascal][Prolog][Tcl/

Tk][Visual_Basic][Enlaces]

http://www.geocities.com/pretabbed/compiladores.htm?20076#VB

Free Byte

Compiladores gratuitos y mucha información y recursos sobre ellos.

www.freebyte.com/programming/

Compilers.net

Incluye estos lenguajes: Ada, Asm, Basic, C/C++, Cobol, Forth, Java, Logo, Modula-2, Modula-3, Pascal, Prolog,

Scheme, Smalltalk.

www.compilers.net/Dir/Free/Compilers/index.htm

TheFreeCountry

www.thefreecountry.com/compilers/cpp.shtml

Bloodshed

Bloodshed desarrolla compiladores gratuitos, como el DEVCPP que indicábamos anteriormente. Ésta es su lista

completa de compiladores.

www.bloodshed.net/compilers/#free_comps

www.bloodshed.net/compilers

Dev-C++ (BloodshedSoftware) // Compiladores gratuitos (Delphi, C/C++, Linux/Unix).

www.freebyte.com/programming

Compilers.net: Compiladores de C/C++, Cobol, Forth, Java, Modula-2, modula-3, Pascal, Smnalltalk, Basic, Ada,

ASM, Javascript, Logo, FORTRAN

www.compilers.net/Dir/Free/Compilers/index.htm

DJGPP/G77

Conocidísimo paquete gratuito (con código abierto) que incluye compiladores de C, C++, FORTRAN y otros, para

diferentes plataformas (Windows, Linux, etc.).

http://www.delorie.com/djgpp/

P:358

762 Fundamentos de programación

C y C++

Compilador GCC de GNU/Linux (Free Software Foundation).

//gcc.gnu.org/onlinedocs/gcc-3.4.3/gcc/

Compiladores Win32 C/C++ de Willus.com.

www.willus.com/ccomp.shtml

Compiladores e intérpretes C/C++.

www.latindevelopers.com/res/C++/compilers

Compilador Lxx-Win32 C de Jacob Navia.

www.cs.virginia.edu/~lcc-win32/

El Rincón del C.

www.elrincondec.com/compile

Visual Studio.

//msdn2.microsoft.com/library/default.aspx

Comeau Computing (compatible C).

www.comeaucomputing.com/features.html

Compiladores Watcom.

//hackindex.com/ karpoff/programación.listas

www.dmoz.org/World/Español

Dev-C++ de BloodshedSoftware.

DEVCPP. Compilador de C++

bloodshed.net/devcpp.html.

www.bloodshed.net/dev/devcpp.html The Dev-C++ Resource Site.

Un entorno integrado de desarrollo IDE (Integrated Development Environment) distribuido con licencia GNU

para la creación de aplicaciones C/C++ utilizando los compiladores GNU gcc/g++ (incluidos en el paquete). Dispone de muchas de las opciones que son frecuentes en otros entornos “de pago”.

Incluyendo, entre otros, un editor altamente configurable con posibilidad de autocompletar las palabras clave, y

de mantener proyectos grandes de distintos tipos: aplicaciones Windows (gráficas); aplicaciones de consola (modo

texto), y construcción de librerías estáticas y dinámicas (DLLs). Existen binarios para su utilización en Windows y

Linux, y cuenta con gran cantidad de módulos adicionales que pueden instalarse selectivamente. Su sistema de actualización on-line y de mantenimiento de paquetes instalados es realmente notorio.

La versión para Windows incluye MinGW, un conjunto de utilidades para desarrollar aplicaciones en Windows

utilizando una interfaz POSIX (Unix/Linux). Es una buena forma de utilizar C++ en Windows utilizando herramientas de código abierto. Por supuesto no esperéis el nivel de sofisticación y refinamiento de otras plataformas “de pago”,

como Builder por ejemplo, pero en ocasiones la simplicidad y la sencillez son más una virtud que un defecto. La

versión Dev-C++ que utilizo es la 4.9.9.2, que incluye la versión 3.4.2-20040916-1 de los compiladores gcc/g++ y

la versión 5.2.1.1 de GDB, que es el depurador GNU.

GCC

El más conocido compilador gratuito de C, GNU Compiler Collection.

http://www.gnu.org/software/gcc/gcc.html

Relo

www.fifsoft.com

P:359

Bibliografía y recursos de programación 763

Si desea desarrollar aplicaciones Windows con el compilador Borland C++ o MinGW, aconsejaría echar un vistazo a esta plataforma. Relo es un sistema integrado de desarrollo de código libre para los compiladores señalados,

aunque la versión actual (2006) permite trabajar también con los compiladores MS Visual C++ y Digital Mars.

(Bjarne Stroustrup) An incomplete list of C++ Compilers.

www.research.att.com/~bs/compilers.html

Lista recomendada de Bjarne Stroustrup, inventor de C++.

Insight sources.redhat.com

Insight es una interfaz gráfica (GUI) de GDB, que es el depurador de GNU. Este producto fue desarrollado inicialmente por Red Hat y donado después al público bajo la GLP (GNU Public License).

Intel (compilador C++ de Intel, plataformas Windows 98, NT, 2000, XP, Vista).

www.intel.com/softwre/products/compilers/cwin

Visual C++ (Visual C++ Developer Center)

//msdn2.microsoft.com/es-es/library/wk21sfcf(vs.so).aspx

Microsoft ha publicado una versión gratuita (de libre descarga desde la Web) de su entorno de desarrollo Visual

C++. Además también está disponible para su descarga una versión del SDK. Es decir, de la documentación necesaria para desarrollar aplicaciones Windows (especialmente interesante porque contiene información sobre la API de

este sistema). Naturalmente está orientado a desarrollos para los entornos Windows.

http://www.microsoft.com/express/vc/

Borland C++ Builder 6

www.borland.com/cbuilder (versión gratuita del compilador de Borland Builder)

Borland C++ Compiler 5.5

Es el mismo que utiliza el “Builder” de este afamado fabricante de software, aunque sin las utilidades “de pago”, que

son fundamentalmente el entorno gráfico de desarrollo y las herramientas RAD. La versión recomendada es una

versión Windows para ser utilizada mediante líneas de órdenes desde el Shell del sistema (una ventana DOS). Está

disponible para su descarga libre desde la Web, aunque para acceder al archivo de instalación (un autoinstalable de

8.52 MB) hay que registrarse. El paquete contiene todas las herramientas para desarrollar aplicaciones C++, incluyendo la Librería Estándar de Plantillas (STL). Existen páginas de ayuda

http://www.codegear.com/downloads/free/cppbuilder

Borland Turbo C++ www.turboexplorer.com

[Delorie] Compilador DJGPP

www.delorie.com/djgpp

Sistema de desarrollo completo de código abierto para construir programas C y C++ 32-bit. El entorno necesita

un PC con procesador Intel 80386 y superior bajo DOS.

Cobol

Muchos bancos, empresas de seguros y otras grandes compañías aún manejan las bases de datos de sus mainframes

mediante este vetusto lenguaje de programación.

http://www.freebyte.com/programming/cobol/cobol650.html

P:360

764 Fundamentos de programación

Delphi

Kylix Open Edition

Compilador de Delphi para Linux, desarrollado por Borland. Incluye herramientas como el debbugger y otras.

Handel

Handel es un compilador de Delphi hecho con... Delphi.

http://homepages.borland.com/torry/tools_compilers.htm

Eiffel

Visual Eiffel

Se dice que es un lenguaje de programación totalmente novedoso y que incluye lo mejor de cada lenguaje de programación clásico, vaya, que según algunos es el lenguaje de programación del futuro. También gratis.

Ensamblador (ASM) NASM

Compilador gratuito de lenguaje ensamblador.

http://www.proc.org.tohoku.ac.jp/befis/download/nasm/

FORTRAN

DJGPP/G77

Paquete gratuito (con código abierto) que incluye compiladores de C, C++, FORTRAN y otros. El de FORTRAN se

llama algo parecido a G77.

http://www.delorie.com/djgpp/

Java

No olvides consultar nuestra sección de Programación en java. Encontrarás recursos java: editores, tutoriales,

applets, etc.

http://www.geocities.com/pretabbed/java.htm

Java Sun

El compilador de lenguaje Java es gratuito y puede descargarse en la web oficial de Sun, companía creadora de este

lenguaje. La última versión ocupa unos 90 MB e incluye compilador (javac) y muchas otras herramientas: intérprete

(java), visualizador de applets (appletviewer), etc.

http://java.sun.com/javase/index.jsp

BlueJ

www.bluej.org

Entorno de desarrollo OO de Java (última versión a primeros de 2008, Bluej 2.1.2)

P:361

Bibliografía y recursos de programación 765

Jikes

Compilador Java alternativo al de Sun, freeware de calidad. Es un proyecto de código abierto que inició IBM (una

de las empresas que más han apoyado el Open Source). Hay versiones para Linux, Windows 95/NT, Solaris/Sparc,

AIX y OS/2.

http://www.ibm.com/sandbox/homepage/version-b/

Excelsior JET Evaluation Package n

Compila códigos fuente Java a ficheros ejecutables (.EXE) para Windows, a diferencia de los demás compiladores

Java, que generan un archivo de bytecodes. (“.CLASS”) la versión Personal Edition es gratis.

http://www.excelsior-usa.com/jetdleval.html

Pizza Comp

Compilador de Java hecho con Java. Es un proyecto de código abierto de SourceForge.net. Gratis.

http://pizzacompiler.sourceforge.net/

Bluette

Complilador gratuito de Java que está especialmente pensado para los que están iniciándose en este lenguaje. Hay

disponible una versión de prueba gratuita (unos 24 MB) en el link referido.

http://www.bluette.com/

The Java Boutique

Permite compilar códigos fuente Java on line. Es decir, te piden que introduzcas el nombre del fichero fuente, y seguidamente te puedes bajar el “.CLASS” compilado. Para los que no puedan tener un compilador java en su máquina.

http://javaboutique.internet.com/compiler.html

Pascal

Borland Community Codegear

Aquí se pueden encontrar versiones anteriores de compiladores de C/C++, Pascal y Turbo Pascal que Borland ha

retirado del mercado y ofrece públicamente.

http://dn.codegear.com/museum

Free Pascal

Uno de los más conocidos compiladores gratuitos de lenguaje Pascal.

http://www.freepascal.org/

Prolog

SWI-Prolog

http://www.swi-prolog.org/

P:362

766 Fundamentos de programación

Visual Basic

XBasic

Otro clon de Visual Basic más sencillo de usar que éste, por lo que es recomendado para los principiantes. No posee

todas las características del Visual Basic.

http://maxreason.com/software/xbasic/xbasic.html

4. RECOMENDACIONES DE LA 3.ª Y ANTERIORES EDICIONES

DE FUNDAMENTOS DE PROGRAMACIÓN

En la página Web del libro puede consultar la bibliografía y recursos Web utilizados y referenciados en la 3.ª edición

y anteriores

www.mhe.es/joyanes

Create a Flipbook Now
Explore more