Subido por Javier Rosales

IntroduccionPascal

Anuncio
Introducción a la Programación.
Pascal.
Vicente López
Escuela Técnica Superior de Informática
Universidad Autónoma de Madrid
( [email protected] )
2
Índice General
1 Esquemas Básicos
1.1 Ordenación temporal . . . . . . . . . . . . . . . . . . . . . . .
1.2 Componentes básicos . . . . . . . . . . . . . . . . . . . . . . .
1.3 Tráfico de información . . . . . . . . . . . . . . . . . . . . . .
2 Lenguajes
2.1 Procesamiento de información:
2.2 Formalización de los lenguajes
2.3 Las máquinas de Von Neuman
2.4 Breve historia de los lenguajes
2.5 Tipos de lenguajes . . . . . .
hombre y ordenador
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
7
8
9
13
13
15
16
17
20
3 Algoritmos
23
3.1 Pasos en la resolución . . . . . . . . . . . . . . . . . . . . . . . 23
3.2 Uso múltiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
3.3 El ejemplo de Josefo . . . . . . . . . . . . . . . . . . . . . . . 25
4 Pascal
4.1 Caracterı́sticas . . . . . . . . . . .
4.2 El programa Pascal . . . . . . . . .
4.3 Palabras reservadas y estructuras .
4.4 Instrucciones sencillas y compuestas
4.5 Diagramas de sintaxis . . . . . . .
.
.
.
.
.
33
33
34
35
37
38
5 Datos
5.1 Variables, datos y direcciones . . . . . . . . . . . . . . . . . .
5.2 Identificadores . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.3 Tipos de datos . . . . . . . . . . . . . . . . . . . . . . . . . .
39
39
40
41
3
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
ÍNDICE GENERAL
5.4
5.5
5.6
5.7
Constates con tipo . . . . . . .
Inicialización de los datos . . .
Asignación de las constantes . .
Asignación de los distintos tipos
. . . . . . .
. . . . . . .
. . . . . . .
de variables
.
.
.
.
.
.
.
.
.
.
.
.
6 Entrada y salida
6.1 Dispositivos de entrada, salida y almacenamiento
6.2 Las funciones Read y Write . . . . . . . . . . . .
6.3 Formatos . . . . . . . . . . . . . . . . . . . . . . .
6.4 Las funciones WriteLn y ReadLn . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
46
46
46
47
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
51
51
52
54
55
7 Acciones
7.1 Operaciones básicas. . . . . . . . . . . . . . . . . . . . .
7.1.1 Operadores aritméticos y expresiones aritméticas.
7.1.2 Funciones aritméticas. . . . . . . . . . . . . . . .
7.1.3 Aritmética entera y real. . . . . . . . . . . . . . .
7.1.4 Operadores lógicos. . . . . . . . . . . . . . . . . .
7.1.5 Expresiones lógicas. . . . . . . . . . . . . . . . . .
7.1.6 Manipulación de bits. . . . . . . . . . . . . . . . .
7.2 Sentencias de control. . . . . . . . . . . . . . . . . . . . .
7.3 Sentencias de repetición. . . . . . . . . . . . . . . . . . .
7.4 Manipulación de los datos STRING. . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
59
59
59
61
62
65
68
70
74
79
85
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
87
87
88
90
91
92
96
97
99
.
.
.
.
103
. 103
. 105
. 106
. 108
8 Modularidad
8.1 Dividir para vencer . . . . . . . . . . . .
8.2 Procedimientos . . . . . . . . . . . . . .
8.3 Funciones . . . . . . . . . . . . . . . . .
8.4 Ámbito de definición de las variables . .
8.5 Paso de valores por contenido o dirección
8.6 Definición diferida . . . . . . . . . . . . .
8.7 Módulos y submódulos . . . . . . . . . .
8.8 Recursividad . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9 Datos con estructura
9.1 Tipos de datos definidos por el programador
9.2 Enumeraciones . . . . . . . . . . . . . . . .
9.3 Conjuntos . . . . . . . . . . . . . . . . . . .
9.4 Arrays . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
ÍNDICE GENERAL
9.5
9.6
5
Registros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114
Uniones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
10 Ficheros
125
10.1 Ficheros con Tipo . . . . . . . . . . . . . . . . . . . . . . . . . 125
10.2 Procesamiento secuencial y aleatorio . . . . . . . . . . . . . . 131
10.3 Ficheros de Texto . . . . . . . . . . . . . . . . . . . . . . . . . 136
11 Punteros
11.1 Contenidos, direcciones e identificadores .
11.2 Punteros . . . . . . . . . . . . . . . . . . .
11.3 Asignación dinámica de memoria. . . . . .
11.4 Declaraciones recursivas de tipos de datos
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
141
. 141
. 142
. 145
. 147
6
ÍNDICE GENERAL
Capı́tulo 1
Esquemas básicos del
ordenador
1.1
Ordenación temporal
Un ordenador es una máquina diseñada para el procesamiento automático
de información.
El esquema más sencillo de un ordenador actual es el siguiente:
Entrada
- Computadora
-
Salida
Se denominan dispositivos de entrada a todos aquellos que hacen posible
la captura de los datos necesarios para realizar las tareas encomendadas al
ordenador. Ejemplos comunes de dispositivos de entrada son los teclados, las
tarjetas perforada, y los ratones, pero también lo son los escaners, micrófonos,
terminales TRC (Tubos de Rayos Catódicos),...
Se denominan dispositivos de salida a todos aquellos que hacen posible
la comunicación de los resultados de las tareas realizadas por el ordenador.
Ejemplos comunes de dispositivos de salida son los terminales TRC, impre7
8
CAPÍTULO 1. ESQUEMAS BÁSICOS
soras, y plotters, pero también lo son altavoces, actuadores mecánicos,...
Es fundamental en un computador la ordenación temporal de sus operaciones:
- Computadora
Entrada
Salida
-
-
Tiempo
y los componentes se definen exclusivamente a partir de la secuencia de operaciones realizadas.
1.2
Componentes básicos
Globalmente a los dispositivos de entrada y salida se les denomina periféricos.
Los dispositivos de almacenamiento de información como discos, diskettes,
cintas,..., también son periféricos que son dispositivos de entrada o salida
según se grabe o se lea la información.
A los componentes fı́sicos del ordenador se le denomina Hardware y a la
información que dirige la realización de las tareas se le denomina Software.
El ordenador, aparte de los periféricos, consta de dos componentes principales: Unidad de memoria y unidad central de procesos.
Ordenador
Unidad
central de
procesos
Unidad de
memoria
• La unidad de memoria está formada por cientos, miles o millones de
celdas de memoria en las que se puede almacenar información y que
están identificadas por su dirección (en el sentido postal).
A
2
4
1
J
3
3
1
2
3
4
5
6
7
u
u
u
1.3. TRÁFICO DE INFORMACIÓN
9
Al contenido de la información en general se accede por la dirección de
la celda. El contenido también puede ser la dirección de una celda con
lo que es posible almacenar en memoria la instrucciones que se han de
realizar incluyendo el acceso a la información de las celdas.
• La unidad central de procesamiento (CPU) dirige y controla el procesamiento de información que realiza el ordenador y consta de dos partes:
la unidad de control y la unidad aritmético – lógica.
unidad
memoria
de
6
?
unidad
de
control
unidad
aritmético
–
lógica
La unidad de control busca, obtiene, y ejecuta las instrucciones de
los programas almacenadas en la memoria. Cuando las instrucciones
indican la realización de operaciones aritméticas (+, ∗,... ) o lógicas (
≥ , ≡ , ...) estas se derivan para ser realizadas en la unidad aritmético
– lógica.
1.3
Tráfico de información
Vamos a ver de forma simplificada las acciones a las que da lugar en un
ordenador las instrucciones de un programa. Es fundamental comprender lo
inexcusable de la secuencialidad temporal de la instrucciones suministradas
al ordenador.
Como ejemplo vamos a considerar esta porción de programa:
{1}
{2}
{3}
{4}
{5}
a := 3;
b := 5;
c := a + b;
c := c + 2;
if (c > 9) then writeln(c);
10
CAPÍTULO 1. ESQUEMAS BÁSICOS
y consideraremos un modelo muy simplificado de un ordenador. Este modelo
dispone de una U.C.P., con unidad de control y aritmético lógica, y una
pequeña memoria con 64 posiciones.
Se reservan las posiciones de la 1 a la 50 para almacenar los programas
que luego se ejecutarán y las posiciones 51, 52, 53, ... se utilizan para datos
intermedios.
En nuestro modelo se inicializan todos los datos a cero al empezar la
ejecución del programa y cuando la U.C.P. va a empezar a ejecutar la primera
instrucción las posiciones 51, 52 y 53 contienen un 0. En esas posiciones se
almacenarán los valores de las variables a,b, y c, respectivamente,
0
0
0
51
52
53
La primera instrucción del programa
a := 3;
indica a la U.C. que en la posición 51 de memoria ha de almacenarse el
número entero 3.
3
0
0
51
52
53
La segunda,
b := 5;
que en 52 almacene un 5,
3
5
0
51
52
53
y en la tercera, la U.C. realiza las siguientes acciones: obtiene los números
almacenados en 51 y 52, los envı́a a la U.A.L. para ser sumados y el resultado
que devuelve se almacena en 53.
3
5
8
51
52
53
1.3. TRÁFICO DE INFORMACIÓN
11
En la cuarta instrucción obtiene el número almacenado en 53 y lo envı́a a
la U.A.L. junto con el 2 para ser sumados. El resultado devuelto lo almacena
de nuevo en 53.
3
5
10
51
52
53
La quinta instrucción implica que la U.C. obtiene el número almacenado
en 53 y lo envı́a junto con el 9 a la U.A.L. para ser comparados. Como
de la comparación la U.A.L. devuelve que es cierto, se envı́a al periférico
correspondiente la orden de sacar al exterior el contenido de la memoria 53.
12
CAPÍTULO 1. ESQUEMAS BÁSICOS
Capı́tulo 2
Lenguajes de programación
2.1
Procesamiento de información: hombre y
ordenador
La información que puede procesar un ordenador es distinta a la que puede
procesar un humano. Aún suponiendo que el procesamiento de información
que realiza el cerebro humano es comparable al que realiza un ordenador,
la diferencia de diseño entre ambos explicarı́a la incompatibilidad. Para el
humano resulta cómodo expresar y pensar conceptos abstractos. La frase
Sumar dos números evoca claramente un operación general que dentro del
formalismo matemático carece de ambigüedad:
z =x+y
.
Es equivalente el valor de la variable z a la suma de los valores
de las variables x e y.
El modo en que esta frase puede traducirse en una orden precisa para que la
realice un ordenador ha cambiado con el tiempo según variaban los diseños de
los ordenadores. Primitivamente en ordenadores como el MARK I se trataba
de una secuencia de tripletes de perforaciones en un cinta. Hoy en dı́a se
trata de las secuencias de ceros y unos del código binario. En el futuro los
programas podrı́an parecerse a los pentagramas de música actuales.
Una lectura parcial de la expresión matemática anterior puede ser la
siguiente:
13
14
CAPÍTULO 2. LENGUAJES
Un valor particular de la variable z se obtiene sumando los valores
de las variables x e y.
En el lenguaje PASCAL la suma de dos números correspondiente a esta
interpretación se expresa con la instrucción
z := x + y ;
con una sintaxis muy próxima a la expresión matemática, si bien su semántica
es distinta pues ha de entenderse del siguiente modo:
El valor almacenado en la memoria identificada con el nombre x
y el valor almacenado en la memoria identificada con el nombre y,
han de sumarse almacenando el resultado en el lugar de memoria
identificado por el nombre z.
Sin embargo, la información que ha de recibir la Unidad Central de Proceso de un ordenador para realizar esta operación puede ser:
0010 0000 0000 0100
0100 0000 0000 0101
0011 0000 0000 0110
lo que en una lenguaje que establece un compromiso entre ordenador y hombre corresponde a
X
Y
@
@
R
@
Z
LOAD
ADD
STORE
X
Y
Z
Este lenguaje se denomina Ensamblador (Assembler) y casi corresponde a
una traducción de la secuencia de ceros y unos en palabras más fáciles de
recordar. Se suele hablar de lenguajes de programación de bajo y alto nivel,
según se acerquen a lenguaje natural de los humanos. El ensamblador es un
2.2. FORMALIZACIÓN DE LOS LENGUAJES
15
lenguaje de programación de bajo nivel y el PASCAL es un lenguaje
de programación de alto nivel. A la secuencia binaria que acepta la UCP
del ordenador se le denomina lenguaje máquina.
La desventaja obvia de un lenguaje de bajo nivel es lo costoso que resulta
la programación de un ordenador con él. A medida que un lenguaje se acerca
más al lenguaje natural, más sencillo es programar con él y más accesible a
personas no especializadas. Pero existe otra desventaja: los lenguajes de
bajo nivel dependen del ordenador. Dado que se han de ceñir al diseño
lógico del ordenador, un lenguaje ensamblador sólo es válido para familias
de ordenadores con el mismo diseño lógico.
2.2
Formalización de los lenguajes
Los lenguajes de alto nivel que vamos a estudiar en este curso son el resultado
de investigaciones realizadas desde dos enfoques distintos. Por una parte, un
lenguaje de programación es un caso particular de un lenguaje formal; por
otra, una solución al problema de ingenierı́a que surge en la construcción de
máquinas procesadoras de información.
La formalización de los lenguajes es un tema de investigación desde los
antiguos griegos. Aristóteles (384–332 A.C.) se puede considerar el padre
de la lógica formal. Leibniz en el siglo XVII, y Frege en el XIX, intentaron construir lenguajes formales sin la imprecisión y ambigüedad de lenguaje
ordinario. Gorge Boole en 1854 proporcionó un nuevo intento de formalización con la introducción de sı́mbolos, fórmulas y axiomas. El método
lógico de Boole permitió construir máquinas lógicas que podı́a resolver automáticamente problemas lógicos. Más tarde, el español Leonardo Torres
y Quevedo (1852–1939), entre otros diseños mecánicos automáticos, construyó el Ajedrecista, un autómata capaz de jugar al ajedrez. A finales del
siglo XIX y en el XX, los lenguajes formales se investigaron con el intento
de formalizar las matemáticas de un modo similar a como Euclides habı́a
formalizado la geometrı́a. Las ideas fundamentales de Russel, Whitehead,
Hilbert, Church, y finalmente Goedel, permitieron establecer la imposibilidad de ese proyecto. Turing y Post en 1936 introdujeron un formalismo de
manipulación de sı́mbolos ( la denominada máquina de Turing ) con el que se
puede realizar cualquier computo que hasta ahora podemos imaginar. Esta
fue una vı́a de comunicación entre los problemas formales de la computación
y de la matemática. La unión permitió demostrar que no existe ninguna
16
CAPÍTULO 2. LENGUAJES
máquina de Turing que pueda reconocer si una sentencia es o no un teorema de un sistema lógico formal; pero también permitió demostrar que si un
cálculo puede explicitarse sin ambigüedad en lenguaje natural, con ayuda
de sı́mbolos matemáticos, es siempre posible programar un ordenador digital
capaz de realizar el cálculo, siempre que la capacidad de almacenamiento de
información sea la adecuada.
Desde el punto de vista de la ingenierı́a, los progresos en lenguajes de programación han sido paralelos a los diseños de los nuevos ordenadores. Babbage ya escribió programas para sus máquinas, pero los desarrollos importantes
tuvieron lugar, igual que en los ordenadores, alrededor de la segunda guerra
mundial. Fue en esa época (justo después de finalizada la guerra) cuando
Zuse publicó su libro Cálculo y programación. En él aparece por primera vez
el concepto de operación de asignación. Zuse se planteó el problema siguiente: la expresión z = z + 1 es incorrecta para significar “ El nuevo valor de z
se obtiene sumando 1 al antiguo ”, e introdujo la expresión z + 1 ⇒ z. Esta
sentencia de asignación nunca se habı́a utilizado antes pues siempre se introducı́a una nueva variable cuando se procedı́a a una asignación ( por ejemplo
y = z + 1). Este nuevo enfoque es fundamental puesto que el uso sistemático
de las asignaciones es lo que distingue la forma de pensar en ciencias de la
computación y en matemáticas.
2.3
Las máquinas de Von Neuman
Originalmente la programación de un ordenador era directamente la reordenación de los componentes del ordenador. La idea de producir un programa
de ordenador que se pudiera almacenarse en la memoria del ordenador se
debe a Von Neuman y apareció en un informe que hizo sobre el ordenador
EDVAC. Von Neuman consideró la posibilidad de que una palabra formada
por 32 bit fuera o bien un número o bien una instrucción. Una instrucción se
codificaba por un grupo de bits adyacentes y consideró sumas, multiplicaciones, transferencia de contenidos de memoria a registros, test, e instrucciones
de bifurcación. Ası́, un programa consistirı́a en una secuencia de palabras en
forma binaria.
Necesidades prácticas muy obvias llevaron a la utilización de mnemotécnicos
para programar las instrucciones, y posteriormente otro programador traducı́a los mnemotécnicos a lenguaje máquina. El paso siguiente debı́a ser
conseguir que fuera el ordenador el que tradujera esas codificaciones y tam-
2.4. BREVE HISTORIA DE LOS LENGUAJES
17
bién lograr que ciertos códigos correspondieran a más de una instrucción
elemental del ordenador. Al principio de los años cincuenta se empezaron a
construir estos Decodificadores o Ensambladores.
El objetivo de los ingenieros que trabajaban en el diseño de los ordenadores era conseguir que el ordenador aceptara instrucciones con un formato
similar al matemático, puesto que en aquella época la mayorı́a de la aplicaciones giraban alrededor de cálculos complejos. Para lograr esto fue necesario
un cambio radical en el enfoque de la operación de un ordenador.
Datos
?
ordenador
?
Resultado
Programa
?
ordenador
?
Programa
El ordenador debı́a considerarse como un procesador de información capaz de transformar un programa escrito en un lenguaje de alto nivel en un
programa en lenguaje máquina. A su vez, debı́a programarse el compilador
capaz de realizar esta transformación.
2.4
Breve historia de los lenguajes
En los años 50 se realizaron varios compiladores primitivos y fue en 1957
cuando apareció el primer compilador de FORTRAN. El compilador de FORTRAN (FORmula TRANslator) estaba diseñado para traducir a lenguaje
máquina expresiones y operaciones matemática, e incluso permitı́a la manipulación de matrices. La aparición del FORTRAN fue un gran cambio para
18
CAPÍTULO 2. LENGUAJES
los programadores que no todos aceptaron de buen grado. No les gustaba
que sus programas fueran tratados por el ordenador como meros datos, y argumentaban que el código máquina generado por el compilador nunca podrı́a
ser tan eficiente como el escrito por ellos directamente. Esto no era generalmente ası́, puesto que el FORTRAN no fue diseñado pensando en crear un
lenguaje bien estructurado sino pensando en crear un traductor de expresiones aritméticas a código máquina muy eficiente. Por ello, el diseño lógico del
ordenador IBM 704 para el que fue creado casi puede deducirse del lenguaje
FORTRAN. En diferentes versiones, cada vez más estructuradas, el lenguaje
FORTRAN se ha utilizado extensivamente desde que apareció hasta hoy en
dı́a, y puede considerarse el lenguaje estandard del cálculo cientı́fico.
Unos años después de aparecer el FORTRAN apareció el lenguaje ALGOL 60 (Algorithm Language), que fue diseñado para ser independiente del
ordenador con una gramática bien definida. También de aquella época es el
COBOL (Common Business Oriented Language) que se diseño para para las
manipulaciones de datos normales en aplicaciones de negocios y con un uso
mayor del lenguaje inglés en sus frases. Las versiones modernas del COBOL
siguen usándose en la actualidad y es el lenguaje estandard en aplicaciones
informáticas bancarias. Desde entonces han aparecido diversos lenguajes de
alto nivel entre los que podemos mencionar el BASIC (Beginners All–purpose
Symbolic Instructional Code), PL/I , APL, PASCAL, ADA, MODULA , C ,
RPG, PROLOG, LISP, ... etc. Alguno de estos lenguajes han sido diseñados
para un tipo concreto de aplicaciones. Por ejemplo, el ADA para aplicaciones relacionadas con defensa, o el RPG para transacciones usuales en los
bancos. La evolución de los lenguajes de programación ha estado guiada por
la evolución de:
• Los ordenadores y sus sistemas operativos.
• Las aplicaciones.
• Los métodos de programación.
• Los fundamento teóricos.
• La importancia dada a la estandarización.
Podemos resumir la evolución de los lenguajes de programación en la siguiente tabla:
2.4. BREVE HISTORIA DE LOS LENGUAJES
periodo Influencias
1950 – 55 Ordenadores primitivos
1956 – 60 Ordenadores pequeños,
caros y lentos
Cintas magnéticas
Compiladores e interpretes
Optimización del código
1961 – 65 Ord. grandes y caros
Discos Magnéticos
Sistemas operativos
Leng. de propósito general
19
Lenguajes
Lenguajes ensamblador
Lenguajes experimentales
de alto nivel
FORTRAN
ALGOL 58 y 60
COBOL
LISP
FORTRAN IV
COBOL 61 Extendido
ALGOL 60 Revisado
SNOBOL
APL ( como notación sólo)
1966 – 70 Ordenadores de diferentes
PL/I
tamaños, velocidades, costes FORTRAN 66 (estandard)
Sistemas de almacenamiento COBOL 65 (estandard)
masivo de datos (caros)
ALGOL 68
S.O. multitarea e
SNOBOL4
interactivos
SIMULA 67
Compil. con optimización
BASIC
Leng. estandard ,
APL/360
flexibles y generales
1971 – 75 Micro ordenadores
Sistemas de almacenamiento PASCAL
masivo de datos pequeños
COBOL 74
y baratos
PL /I
Progr. estructurada
Ingenierı́a del software
Leng. sencillos
1976 – 80 Ord. baratos y potentes
ADA
Sistemas distribuidos
FORTRAN 77
Prog. tiempo–real
PROLOG
Prog. interactiva
C
Abstracción de datos
Prog. con fiabilidad
y fácil mantenimiento
20
CAPÍTULO 2. LENGUAJES
2.5
Tipos de lenguajes
Desde un punto de vista más general los lenguajes se pueden clasificar en
lenguajes de procedimiento o declarativos. En los primeros, con el lenguaje se
especifica paso a paso el procedimiento que ha de realizar el ordenador para
procesar la información, mientras que en los segundos se declaran hechos
que han de dirigir las respuestas del ordenador. El PASCAL y el C que
estudiaremos en este curso son de procedimiento, mientras que por ejemplo
el PROLOG, es declarativo.
Una porción de programa PROLOG es ası́:
. . .
hijo(X,Y) <- padre(Y,X) , varon (X).
hija(X,Y) <- padre(Y,X) , hembra (X).
abuelo(X,Z) <- padre(X,Y) , padre (Y,Z).
. . .
estableciendo relaciones lógicas que determinan una base de verdades con las
que han de ser coherentes las respuestas del programa.
Exite una diferencia grande en la programación con un lenguaje u otro
según sea interpretado o compilado, si bien esta distinción puede no ser inherente al lenguaje sino a su puesta en práctica en un determinado ordenador.
Un lenguaje es interpretado cuando la transformación de las instrucciones de
alto nivel a lenguaje máquina se realiza sentencia a sentencia según se van
ejecutando. Un lenguaje es compilado cuando esta trasformación se realiza
en bloque antes de que ninguna instrucción sea ejecutada. Por ejemplo, el
BASIC en general se suele interpretar y el LISP siempre. En un lenguaje
interpretado la puesta a punto de un programa ha de realizarse secuencialmente puesto que las distintas partes del programa no se pueden verificar
hasta que entran en ejecución.
En el caso más común de lenguajes compilados, son varios los subprocesos
implicados en la transformación del código de alto nivel en instrucciones
ejecutables por la UCP.
2.5. TIPOS DE LENGUAJES
21
Programa
COMPILADOR
Leng. maquina
H
HH
HH
L. maquina
HH
H
HH
HH
MONTADOR
Codigo UCP
A su vez el compilador realiza varias tareas:
• verificación de la sintaxis
• transformación a código máquina
• optimización
Si un programa es incorrecto sintácticamente, el compilador detectará el error
y lo comunicará con un mensaje relacionado con la regla del lenguaje que
se ha incumplido. Normalmente, estos son los errores más comunes y más
fáciles de detectar.
El montador (Linker) unifica el código con el proveniente de otros subprogramas con el que se intercambian datos. Para ello realiza una lista de los
22
CAPÍTULO 2. LENGUAJES
datos que comparten todos los programas y asigna las direcciones comunes
donde cada uno deberá procesar esos datos. Esta unificación de direcciones
será imposible si algún procedimiento supuestamente existente en otro subprograma no aparece o aparece de un modo no unı́voco. También será causa
de error que algún dato compartido por subprogramas esté declarado de modo distinto en cada programa. Estos errores son poco comunes y fácilmente
detectable con los mensajes de error proporcionados por el montador. Sin
embargo, los errores más comunes y más tediosos de eliminar son aquellos
de programación que dan lugar a sentencias sintácticamente correctas pero
que corresponde a acciones distintas a las deseadas. Desgraciadamente sólo
se detectan en la ejecución del programa.
En la actualidad existe la posibilidad de utilizar depuradores de programas (llamados en inglés debuggers por el origen de los errores en los ordenadores primitivos ) que permiten seguir la ejecución paso a paso de un
programa aunque se obtenga el código máquina por compilación. Esta herramienta facilita enormemente la depuración de los programas pues permite
conocer o modificar el valor de los datos manipulados en el programa durante
la ejecución.
Capı́tulo 3
Algoritmos y resolución de
problemas con el ordenador
3.1
Pasos en la resolución
Vamos a analizar las caracterı́sticas del procedimiento de resolución de un
problema con el ordenador. La mayorı́a de las consideraciones son válidas
para problemas genéricos y muy pocas son especı́ficas de esta herramienta.
Etapas del procedimiento de resolución:
1. Análisis del problema.
2. Realización de la estrategia ideada para su solución.
3. Verificación y análisis del rendimiento del procedimiento.
Cada una de estas etapas consta de varias tareas:
1. Análisis del problema.
• Comprensión del problema.
Pues de lo contrario podemos acabar resolviendo magistralmente
un problema distinto.
• Especificación de los datos de entrada.
23
24
CAPÍTULO 3. ALGORITMOS
• Diseño del esquema del algoritmo que satisface las restricciones
del problema.
ALGORITMO: Procedimiento para resolver un problema paso a paso.
ALGORITMO: Conjunto de operaciones
que secuencialmente conducen a la respuesta
a una pregunta en un número finito de pasos.
• Especificación del modo de proporcionar la respuesta al problema
planteado
2. Realización de la estrategia ideada para su solución.
• Captura de datos.
• Generación de las estructuras de datos adecuadas para el algoritmo que se va a utilizar.
• Especificación de los algoritmos.
• Presentación de los resultados.
Cuando la herramienta que se utiliza es el ordenador, la estrategia se
ha de realizar en tres pasos:
(a) DISEÑO.
(b) ESCRITURA.
(c) VERIFICACIÓN.
3. Verificación y análisis del rendimiento del procedimiento.
• Prueba con casos sencillos.
• Prueba con casos complejos.
• Prueba con casos extremos.
• Análisis del rendimiento en casos poco favorables y en casos tı́picos.
• Refinamiento de los algoritmos.
• Refinamiento de la escritura de los algoritmos.
3.2. USO MÚLTIPLE
3.2
25
Uso múltiple
El uso múltiple de los procedimientos para la resolución de problemas con
el ordenador implica que los procedimientos han de cumplir los siguientes
requisitos:
• La resolución de un mismo problema con distintos datos y en periodos
largos de tiempo obliga a:
1. Realización de procedimientos muy bien verificados.
2. Realización de procedimientos muy bien documentados y escritos
con un estilo claro.
• La resolución de un mismo tipo genérico de problemas o variantes del
mismo, con reutilización de las herramientas desarrolladas, obliga a:
1. Realización de procedimientos modulares.
2. Realización de procedimientos muy bien documentados y escritos
con un estilo claro.
• El uso del procedimiento dentro de un equipo por distintas personas
para resolver un mismo problema o variantes de él, obliga a:
1. Realización de procedimientos muy bien verificados.
2. Realización de procedimientos modulares.
3. Realización de procedimientos muy bien documentados y escritos
con un estilo claro.
3.3
El ejemplo de Josefo
Vamos a ver el significado de todas estas consideraciones con un ejemplo:
una variante del Problema de Josefo (Flavio Josefo, I DC):
Un grupo de personas prefieren el suicidio a la esclavitud y deciden colocarse en cı́rculo eligiendo siempre al siguiente como ejecutor y asesinando al situado después del ejecutor. Ası́ sucesivamente hasta que quede un sólo ciudadano que deberı́a suicidarse.
El problema de Josefo es conocer en qué lugar ha de colocarse en
el corro para quedar el último y reflexionar libremente sobre la
decisión colectiva.
26
CAPÍTULO 3. ALGORITMOS
Se pretende disponer de un programa en Pascal capaz de proporcionar rápidamente
la posición privilegiada.
1. Análisis del problema.
• Comprensión del problema.
Es muy útil empezar con un caso sencillo.
3
2
'$
4
-1
&%
5
6
• Especificación de los datos de entrada.
En este caso es sólo el número de individuos en el cı́rculo.
• Diseño del esquema del algoritmo que satisface las restricciones
del problema.
Si es posible utilizar una estructura de datos de lista circular el
algoritmo es una copia del método gráfico de resolver el problema.
• Especificación del modo de proporcionar la respuesta al problema
planteado
La respuesta es comunicar el número de orden del último individuo
en el cı́rculo.
2. Realización de la estrategia ideada para su solución.
• Captura de datos.
3.3. EL EJEMPLO DE JOSEFO
27
{ Lectura del numero de personas en el circulo }
Readln(n);
• Generación de las estructuras de datos adecuadas para el algoritmo que se va a utilizar.
Se define la estructura de datos más adecuada
{ el tipo NODO es un registro que contiene el ordinal y un
}
{ enlace al siguiente nodo. Se formara una lista circular de nodos }
type enlace = ^nodo;
nodo = record
numero: integer;
siguiente: enlace
end;
var
j , n : integer;
individuo , temporal: enlace;
y se rellena
{Se construye la lista circular }
{ Se genera el nodo de cabeza y se guarda en TEMPORAL
{ para luego poder cerrar el circulo
new(individuo);
individuo^.numero := 1;
temporal := individuo;
}
}
{ Se generan los restantes nodos hasta completar los N nodos}
for j := 2 to N do begin
new(individuo^.siguiente);
individuo := individuo^.siguiente;
individuo^.numero := j;
end; {endfor}
{Se cierra la lista circular al apuntar con el n-esimo }
{nodo al primero
}
individuo^.siguiente := temporal;
28
CAPÍTULO 3. ALGORITMOS
• Especificación de los algoritmos.
{Algoritmo de eliminacion de individuos de la lista circular}
{Mientras quede mas de un solo nodo en la lista se eliminan }
while ( individuo <> individuo^.siguiente ) do begin
{se salta al ejecutor }
individuo:= individuo^.siguiente;
{se avisaria del individuo que seria eliminado }
{ writeln (individuo^.siguiente^.numero);}
{se almacena el nodo que se va eliminar para luego liberarlo }
{de la memoria}
temporal := individuo^.siguiente;
{Se une el antecesor del nodo fiambre a su sucesor}
individuo^.siguiente := individuo^.siguiente^.siguiente;
{y la liberacion de la posicion de memoria del nodo completa la}
{ejecucion
}
dispose(temporal);
end; {endwhile}
• Presentación de los resultados.
{Se comunica el orden del ultimo en el circulo }
writeln (individuo^.numero);
El programa completo es:
Program Josefo (input,output);
{ el tipo NODO es un registro que contiene el ordinal y un enlace }
{ al siguiente nodo. Se formara una lista circular de nodos
}
type enlace = ^nodo;
3.3. EL EJEMPLO DE JOSEFO
29
nodo = record
numero: integer;
siguiente: enlace
end;
var
j , n : integer;
individuo , temporal: enlace;
begin { Josefo }
{ Lectura del numero de personas en el circulo }
Readln(n);
{Se construye la lista circular }
{ Se genera el nodo de cabeza y se guarda en TEMPORAL
{ luego poder cerrar el circulo
new(individuo);
individuo^.numero := 1;
temporal := individuo;
para }
}
{ Se generan los restantes nodos hasta completar los N nodos}
for j := 2 to N do begin
new(individuo^.siguiente);
individuo := individuo^.siguiente;
individuo^.numero := j;
end; {endfor}
{Se cierra la lista circular al apuntar con el n-esimo nodo al primero}
individuo^.siguiente := temporal;
{Algoritmo de eliminacion de individuos de la lista circular }
{Mientras quede mas de un solo nodo en la lista se eliminan
while ( individuo <> individuo^.siguiente ) do begin
{se salta al ejecutor }
individuo:= individuo^.siguiente;
}
30
CAPÍTULO 3. ALGORITMOS
{se avisaria del individuo que seria eliminado }
{ writeln (individuo^.siguiente^.numero);}
{se almacena el nodo que se va eliminar para luego liberarlo }
{de la memoria}
temporal := individuo^.siguiente;
{Se une el antecesor del nodo fiambre a su sucesor}
individuo^.siguiente := individuo^.siguiente^.siguiente;
{y la liberacion de la posicion de memoria del nodo completa la}
{ejecucion
}
dispose(temporal);
end; {endwhile}
{Se comunica el orden del ultimo en el circulo }
writeln (individuo^.numero);
end. { Josefo }
3. Verificación y análisis del rendimiento del procedimiento.
• Prueba con casos sencillos.
Se probarı́a con N = 6 para el que se conoce bien el resultado.
• Prueba con casos complejos.
Se probarı́a con N grande.
• Prueba con casos extremos.
Se probarı́a con N = 1.
• Análisis del rendimiento en casos poco favorables y en casos tı́picos.
En este caso el algoritmo utilizado crece linealmente con el número
de individuos considerado.
• Refinamiento de los algoritmos.
En este caso un análisis más detallado del problema lleva a un
algoritmo mucho mas eficiente y cuyo costo computacional es independiente del número N de elementos en el cı́rculo:
3.3. EL EJEMPLO DE JOSEFO
31
Si se descompone N de la forma N = 2m + l el último elemento
que se elimina es el 2l + 1.
• Refinamiento de la escritura de los algoritmos.
Uso múltiple del programa.
• Verificación.
La verificación a menudo debe incluir pruebas para detectar el mal uso
del programa por parte de un usuario que no esta familiarizado con el
programa o con el problema.
En el programa Josefo la captura de datos de datos podrı́a incluirse
una especificación del dato requerido y una verificación del valor.
{ Lectura del numero de personas en el circulo }
writeln(’ Teclee el número de elementos en el cı́rculo ’);
Readln(n);
if (n <1) then begin
writeln(’ Ha de ser un numero mayor que cero’);
Halt;
end; {endif}
• Modularidad.
• Documentación.
• Claridad de estilo.
El programa Josefo si se acompaña de la adecuada documentación
externa permite su utilización para distintos datos y en tiempos futuros.
Su modificación para variantes del mismo problema es sencilla. Imaginemos que el problema se modifica y se eliminan elementos saltando
cada vez M individuos en vez de 2. Aparte de la modificación en la
entrada de datos , la modificación del algoritmo es mı́nima y sencilla
de localizar:
{se salta al ejecutor }
individuo:= individuo^.siguiente;
32
CAPÍTULO 3. ALGORITMOS
se modificarı́a a:
{se salta al ejecutor }
for j := 1 to ( M -1 )
do begin
individuo:= individuo^.siguiente;
end; {endfor}
Esta modificación serı́a igual de fácil para el autor original del programa
como para cualquier otro programador.
Capı́tulo 4
Pascal y Turbo Pascal
4.1
Caracterı́sticas
El PASCAL es un lenguaje relativamente moderno, desarrollado por Niklaus
Wirth y su grupo de Zurich en 1971. Se trata de un lenguaje de propósito
general, esto quiere decir que se puede emplear para construir todo tipo de
aplicaciones. En la práctica también quiere decir que se trata de un lenguaje
no diseñado para desarrollar ningún tipo especı́fico de aplicaciones. Pero el
PASCAL es especialmente útil para algo: para la enseñanza de buenos modos
de programación. El PASCAL es hoy en dı́a el lenguaje más usado para la
enseñanza de la programación por varios motivos:
• Posee unas reglas sencillas de sintaxis.
• Es un lenguaje muy estructurado.
• Realiza una comprobación exhaustiva de tipos de datos.
El hecho de que tenga una estructuración muy marcada permite que los programas sean fáciles de leer e interpretar, y facilita la escritura de programas
del modo que hoy en dı́a se estima correcto.
El compilador de PASCAL es relativamente sencillo de realizar, por lo
que se ha extendido a muchos tipos de plataformas, desde los ordenadores
personales a los grandes ordenadores corporativos. Cuando una aplicación
se escribe en PASCAL estandard puede compilarse en cualquier máquina en
la que exista compilador de PASCAL , que son la mayorı́a.
33
34
CAPÍTULO 4. PASCAL
Existen varios dialectos locales del PASCAL , entre los que se encuentra el
TURBO PASCAL , que admiten todas las instrucciones del PASCAL estandard más un subconjunto especı́fico de instrucciones normalmente pensadas
para aumentar las capacidades del lenguaje en un ordenador particular.
El TURBO PASCAL , de la compañı́a Borland (Scotts Valley, California) es un dialecto del PASCAL que incluye además de las instrucciones del
PASCAL estandard una serie de instrucciones que permiten desarrollar aplicaciones especı́ficas para ordenadores IBM PC o IBM PS y compatibles. No
es la única de las versiones de PASCAL existente para estos ordenadores,
pero sin duda la más extendida y probada. En la actualidad la versión 6.0 de
este lenguaje incluye además del compilador un entorno integrado de desarrollo (IDE) que permite compilar, visualizar errores y depurar los programas
desde un mismo entorno.
4.2
El programa Pascal
Un programa PASCAL es un conjunto de instrucciones que siguen la sintaxis
y la estructura del PASCAL . La estructura genérica es:
Program nombre (ficheros);
. . .
declaraciones
. . .
Begin
. . .
sentencias
. . .
End.
Todo programa Pascal empieza con la palabra Program seguida de un
nombre que elige el programador para identificar el programa. A continuación
entre paréntesis se pueden indicar los ficheros que contienen los datos de
entrada y salida respectivamente. Estos ficheros son el input y el output
para indicar entrada desde el teclado y salida al terminal. Si se quieren
especificar estos ficheros la primera lı́nea de un programa será:
Program nombre (Input , Output );
4.3. PALABRAS RESERVADAS Y ESTRUCTURAS
35
y es equivalente a:
Program nombre ;
La primera lı́nea del programa es una instrucción PASCAL y como todas
ellas termina con el signo de puntuación “ ; ” .
Después de la identificación del programa se han de situar las instrucciones declarativas del programa que sirven para especificar sin ambigüedad el
significado de los términos que se utilizarán en el programa. A continuación
han de aparecer las instrucciones correspondientes al procedimiento que se
quiere realizar. Esta instrucciones están encabezadas por Begin y terminan
con End y un punto. El programa más pequeño y más inútil que cumple las
reglas de estructuración del PASCAL es:
Program nulo;
{ Programa ejemplo de la estructura
más simple de un programa PASCAL }
Begin
(* No hace falta ninguna instrucción
para no hacer nada
*)
End.
En la parte reservada a declaraciones no se incluye nada pues nada se
necesita declarar. Todos los sı́mbolos que se encuentren entre los paréntesis
{ } son comentarios que sirven para hacer más legible el programa. También
los sı́mbolos compuestos (* y *) sirven para delimitar el principio y fin de un
comentario. Al existir dos tipos de delimitadores de comentarios, es posible
realizar anidación de comentarios. Por ejemplo,
{ Este es un comentario
(* sintácticamente *)
correcto en PASCAL }
4.3
Palabras reservadas y estructuras
Las palabras Program, Begin, End, Input, y Output, y otras que veremos más adelante, son identificadores que permiten al compilador PASCAL
36
CAPÍTULO 4. PASCAL
interpretar el programa y no se pueden utilizar con otros fines. Estas palabras
se suelen referir con el término Palabras reservadas. El compilador interpreta
igual mayúsculas o minúsculas, y por tanto se pueden usar indistintamente.
Esta libertad de elección debe utilizarse para aumentar la legibilidad de los
programas. Por ejemplo, podemos empezar siempre con mayúscula las palabras reservadas y ası́ será más fácil detectar las estructuras del programa.
También suele escribirse todo con mayúsculas el nombre de las constantes
cuyo valor no puede alterarse durante el programa. La elección de un estilo
u otro de escritura suele variar pero lo importante es que sea uniforme a lo
largo del programa.
En la parte del programa reservada para declaraciones se incluyen diversos
tipos de declaración:
{1}
{2}
{3}
Program nombre ;
(* Este es un ejemplo de la estructura de la
parte declarativa de un programa PASCAL *)
{4}
Uses nombre unidades ;
{5}
{6}
Const
nombre constante = valor ;
. . .
Type
nombre tipo = def tipo ;
. . .
Var
nombre variable : tipo dato ;
. . .
Begin
. . .
{7}
{8}
{9}
{10}
{11}
La palabra reservada Uses de la instrucción {4} se utiliza para especificar
el nombre las unidades donde se almacenan otros trozos de código PASCAL
que son necesarias para completar el código especificado en este programa.
Normalmente se trata de porciones de código que forman parte de una librerı́a
de programas. En la instrucción {5} la palabra reservada Const indica
que se ha acabado la parte iniciada por Uses y se empieza la parte de
declaración de las constantes que aparecen en el programa. Con Type se
4.4. INSTRUCCIONES SENCILLAS Y COMPUESTAS
37
inicia la declaración de tipos de datos definidos por el programador, y con
Var la especificación del tipo de dato que corresponde a las variables que se
utilizan en el programa. Finalizada esta parte declarativa, empezarı́an las
instrucciones del procedimiento que se está programando ({11}). Las lı́neas
de código {1} , {4} , {6} , {8} y {10} corresponden a sentencias PASCAL
y por tanto acaban con el signo de puntuación “;”. Aparte de estos tipos de
declaración también existen las de etiquetas, procedimientos y funciones.
4.4
Instrucciones sencillas y compuestas
El cuerpo de instrucciones del procedimiento está formado por sentencias que
pueden ser sencillas o compuestas. Las sentencias sencillas acaban con “;”
y las compuestas empiezan por Begin y acaban con End. Por ejemplo,
{1}
{2}
If (a > 0) Then
b := a *
a * a ;
la sentencia {2} es sencilla y está delimitada por la palabra reservada Then
de la construcción condicional iniciada por If, y por el punto y coma. Esta
sentencia sencilla es completamente equivalente a la compuesta especificada
entre {4} y {7}:
{3}
{4}
{5}
{6}
{7}
If (a > 0) Then
Begin
b := a * a ;
b := b * a
End;
El principio de la sentencia es el Begin de {4}, y el final el End de {7}.
La sentencia compuesta consta de dos sencillas. La especificada en {5} se
termina con la puntuación “;” mientras que la de {6} no necesita otra finalización que el End. Ası́ mismo, la sentencia compuesta ha de finalizarse
con el ; situado después del End en {7}. En cualquier lugar de un programa PASCAL donde puede incluirse una sentencia sencilla también puede
incluirse una compuesta.
38
CAPÍTULO 4. PASCAL
4.5
Diagramas de sintaxis
La forma más escueta de representar las construcciones sintácticamente válidas
del PASCAL es utilizar los diagramas de sintaxis. La especificación de las
dos opciones que existen, sentencias sencillas o compuestas, serı́a ası́ con un
diagrama de sintaxis:
Instrucción sencilla
-
E
E
E
E
E
E
E
E
E
Begin
Instruc. senc.
J
J
J
End -
;
Este diagrama ha de leerse siguiendo las fechas de izquierda a derecha y
siendo válido cualquier camino en toda bifurcación. Una construcción estará
representada por un diagrama de sintaxis cuando empezando por la izquierda
se puede salir por la derecha y cada una de las partes de la construcción corresponde a las especificaciones que se encuentran en el camino. Por ejemplo,
las sentencias presentadas en los ejemplos anteriores son correctas, mientras
que la siguiente:
{4}
{5}
{6}
{7}
Begin
b := a * a ;
b := b * a ;
End;
es incorrecta. Si seguimos el diagrama nos encontramos que después del
punto y coma, al final de {6}, deberı́a haber otra sentencia sencilla mientras
que existe un End.
Capı́tulo 5
Datos
5.1
Variables, datos y direcciones
En este tema vamos a ver las instrucciones necesarias en un programa PASCAL para especificar los tipos de datos que se van a manejar. Los datos se
almacenarán en la memoria del ordenador en una determinada posición, pero
para referirnos a ellos se utilizan nombres que el programador elige libremente. El lenguaje PASCAL permite utilizar cómodamente una gran variedad
de tipos de datos, pero en este tema solo vamos a ver los más sencillos.
3.14159
6
nombre
PI
contenido
-
7161
dirección
Las primeras instrucciones de un programa han de ser las que indican los
tipos de datos que se van a utilizar. Cuando un nombre se utiliza para un
dato que no va a modificarse se trata de un a constante. Si por el contrario
se permite que el contenido de las posiciones de memoria referidas por un
determinado nombre varı́en durante la ejecución del programa, se trata de
una variable.
Program Uno;
Const
39
40
CAPÍTULO 5. DATOS
PI = 3.14159;
UNIDADES = ’ radianes ’;
Var
n , m : Integer;
z
: Real;
Begin
. . . .
Este es un comienzo válido de programa en el que se especifican datos y
variables que se van a utilizar. La sección donde se agrupan las definiciones
de constantes se encabeza con el indicativo Const, la sección de variables con
Var, y ambas han de preceder el indicativo Begin que anuncia el comienzo
de las instrucciones del programa que van a manipular los datos. En el
apartado de constantes para asignar el valor se utiliza el operador = , en
lugar del operador normal de asignación := .
5.2
Identificadores
Los nombres que se pueden utilizar para referirse tanto a las variables como
a las constantes son todos aquellos que no establezcan conflicto con las palabras reservadas en PASCAL para especificar datos o acciones. NO pueden
utilizarse:
• Palabras reservadas
Begin
1.23E2
123
While
Cos
• Palabras que empiecen por números
1YA
2ABC
• Palabras en las que se encuentre alguno de los indicadores de operadores
PASCAL
x+y
precio/calidad
• Palabras separadas por blancos.
signo*valor
5.3. TIPOS DE DATOS
41
Se pueden utilizar indistintamente mayúsculas o minúsculas pero el compilador de PASCAL no las distinguirá.
Son válidos nombres como
a364b46
coco
xxx2
pero se deben utilizar nombre que tengan relación con el dato que van a
contener. Por ejemplo,
angulo
lado
vertice
CaraOpuesta
Algunos compiladores de PASCAL solo admiten números y letras en la composición del nombre de datos y variables. El TURBO PASCAL también
admite otros caracteres. Por ejemplo, permite
Cara_Opuesta
Nombre_Compuesto
En el ejemplo Uno podemos ver que el tipo de datos que van a contener
las constantes se especifica por los valores que se asignan.
Program Uno;
Const
PI = 3.14159;
UNIDADES = ’ radianes ’;
Var
n , m : Integer;
z
: Real;
Begin
. . . .
Sin embargo, el tipo de las variables ha de indicarse explı́citamente con indicadores PASCAL tales como Integer, o Real. Estos se refieren a dos tipos
de datos numéricos entre los posibles en PASCAL .
5.3
Tipos de datos
Los tipos de datos sencillos son:
1. Numéricos.
• Números enteros:
42
CAPÍTULO 5. DATOS
–
–
–
–
–
Integer
Byte
ShortInt
Word
LongInt
• Números reales:
–
–
–
–
Real
Double
Single
Extended
2. Caracteres y alfanuméricos.
• Char
• String
3. Valores de la lógica de Boole
• Boolean
• Integer
Es el tipo de variable que se usa para números enteros ( sin parte
decimal ) con signo. Para una variable Integer se reservan dos bytes
(16 bits) en memoria y puede almacenar números enteros en el rango
entre
−32, 768(−215 )
+ 32, 767(215 − 1)
y
Es el tipo de variable utilizado normalmente para las operaciones aritméticas
entre enteros.
• LongInt
Para aquellos casos en los que es necesario utilizar enteros que exceden
el lı́mite aceptado por el tipo Integer, existe el tipo LongInt para el
que se reservan 4 bytes en memoria. Con una variable de tipo LongInt
se pueden referenciar números enteros con signo en el rango entre
−2, 147, 483, 648(−231 )
y
+ 2, 147, 483, 647(231 − 1) .
5.3. TIPOS DE DATOS
43
• Word
En este tipo de datos se pueden almacenar enteros sin signo. Al igual
que para el tipo Integer, para el tipo Word se reservan 2 bytes en
memoria y puede admitir números enteros entre 0 y 65, 535.
• Byte
En PASCAL es posible utilizar un tipo de dato llamado Byte, para el
que , como su nombre indica, sólo se reserva 1 byte de memoria. En
las variables tipo Byte se pueden almacenar números enteros sin signo
y por tanto tendrán que estar limitados entre 0 y 255(28 − 1).
• Real.
Para almacenar números reales ( con parte decimal ) o enteros que
excedan el lı́mite permitido por LongInt , se ha de utilizar el tipo de
variable Real . A este tipo de datos también se le conoce con el nombre
de coma flotante por el uso que se hace de los 6 bytes que se reservan
en memoria para este tipo de datos. Con los 6 bytes y una forma de
representar el número algo rebuscada que estudiaremos con los errores numéricos, se pueden almacenar números entre 2, 910−39 y 1, 71038
tanto positivos como negativos. Debido a la representación interna
utilizada, se almacenan con igual precisión (7 u 8 cifras significativas)
todos los números reales en el rango permitido.
Las operaciones con números en la representación en coma flotante
son mucho más lentas que entre números enteros representados directamente con su valor en base 2. Un coprocesador matemático (como
los 80X87) se dedica especı́ficamente a estas operaciones. El TURBO
PASCAL permite una colección de datos para tratar eficientemente con
números reales cuando se dispone del coprocesador matemático. Cuando no se dispone de él, las operaciones se pueden emular por software
aunque son más lentas. Esta colección de tipos de datos son:
– Single
El tipo de datos Single es un Real más corto (4 bytes) con el
mismo rango de variación que el Real , pero con menos cifras
significativas.
– Double
44
CAPÍTULO 5. DATOS
Se trata de un Real largo (8 bytes) que acepta números reales
entre 10−308 y 10308 y opera con 15 o 16 cifras significativas.
– Extended
Esta es la elección si el tiempo de cálculo no es problema y prima
la precisión. Se trata de un tipo de dato para el que se reservan
10 bytes en memoria y puede almacenar números reales desde
3.410−4932 y 1.1104932 . Se pueden distinguir números que tienen
19 0 20 dı́gitos iguales.
• Boolean
Los valores que puede tomar una variable lógica, dentro de la lógica
Booleana (George Boole, Inglaterra 1815 – 1864), son Verdadero o Falso. En PASCAL se suelen utilizar este tipo de variables para almacenar
el resultado comparaciones o el establecimiento de condiciones. Su dos
valores posibles son True y False.
Var
mayor , menor : Boolean;
{ ......
}
begin
{ ......
}
mayor := True;
menor := False;
{ ......
}
• Char
Para un dato del tipo Char se reserva un solo byte de memoria. En
ese byte se puede almacenar información de un carácter alfanumérico.
Si bien en la memoria del ordenador se está almacenando un número
entero entre 0 y 255, este número no puede entrar a formar parte de
operaciones aritméticas con números enteros pues se entiende que se
trata de un carácter ASCII. El numero almacenado es el ordinal del
carácter en la tabla ASCII.
• String
Cuando se quieren manipular grupos de caracteres ordenados, como
por ejemplo en texto, se dispone del tipo de datos String. En una
5.3. TIPOS DE DATOS
45
variable del tipo String se pueden almacenar entre 1 y 255 caracteres
de los que se almacenan en una variable var . Por tanto el numero de
bytes que se reservan para este tipo de datos dependerá de caso y el
programador tendrá que especificarlo.
En este ejemplo se puede ver como se realiza la especificación:
Program Dos;
Const
Lugar = ’modulo’;
Var
facultad : Char;
modulo , clase : String[6];
begin
{ ......
}
facultad := ’C’;
modulo
:= ’-XVI’;
clase
:= facultad + modulo;
writeln(’ Sera en el ’,Lugar,’ ’,clase);
{ .......
}
end.
El lugar que ocupa en memoria una variable String[N], no son N bytes,
sino N +1. Esto es debido a que en el primer byte se almacena el número
actual de componentes. En el ejemplo anterior, el espacio reservado
para modulo se distribuye:
4
-
X
V
I
%
A
Los compiladores PASCAL no inicializan las variables al principio de la
ejecución del programa, por lo que en los espacios de memoria reservados y no rellenados se puede encontrar cualquier tipo de información.
Si bien se necesita un byte de almacenamiento extra por String, el
procesamiento de estas cadenas de caracteres puede ser muy eficiente.
46
CAPÍTULO 5. DATOS
5.4
Constates con tipo
Cuando se quiere utilizar en un programa una variable pero su valor se quiere
asignar antes de cualquier operación, se puede hacer uso de las Constates
con tipo. Se trata más que de verdaderas constantes, de variables con
asignación inicial de valor. El contenido en memoria de una constante con
tipo puede modificarse con las instrucciones del programa. Su sintaxis es
una mezcla de la asignación de tipo de las variables y de la definición de
constantes. El programa Tres produce el mismo resultado que Dos.
Program Tres;
Const
lugar : String[13] = ’modulo’;
begin
{ ......
}
lugar := lugar + ’ C’+ ’-XVI’;
writeln(’ Sera en el ’,lugar);
{ .......
}
end.
5.5
Inicialización de los datos
Dado que en PASCAL no se inicializan a cero , falso, o caracteres blancos, las
variables cuando se define su tipo, hay que tener precaución de no utilizar el
contenido de posiciones de memoria reservadas y no asignadas. Una medida
tajante es inicializar todas las variables.
5.6
Asignación de las constantes
Cuando se asigna el valor a una constante no se identifica explı́citamente el
tipo de dato del que se trata. Esta identificación se realiza por el contenido del
término a la derecha del operador =. Tanto en la asignación de constantes,
como en la de variables en el cuerpo del programa, el compilador decide el
espacio de memoria que ha de reservar para cada valor por el modo en que
está escrito.
5.7. ASIGNACIÓN DE LOS DISTINTOS TIPOS DE VARIABLES
47
Const
lugar = ’modulo’;
En este caso, lugar es una constante del tipo String por que le asigna un
valor que es un conjunto de caracteres separados por comas.
Const
facultad
= ’C’;
Cuando entre comas se sitúa un sólo carácter, se trata del valor de una
constante Char . También es sencillo reconocer los datos Boolean porque
se les asigna el indicador True o False . En el caso de los números, la
distinción se realiza entre enteros y reales. Para las constantes asignadas
con números reales, el compilador elige el tipo Real, y para los enteros el
Integer. La distinción entre números enteros y reales se hace por existencia
de punto decimal en el valor. Para los números reales existe también la
posibilidad de notación cientı́fica.
Const
Pi = 3.14159;
PiMedios = 1570.8e-3;
PiGrados = 180 ;
El compilador PASCAL interpreta que PiGrados es una constante Integer
, y las otras dos constantes del tipo Real .
5.7
Asignación de los distintos tipos de variables
En el cuerpo del programa se suelen asignar valores a variables. En este
caso, a partir de las instrucciones de la sección destinada a la identificación
del tipo de variables, se reserva el espacio adecuado de memoria para cada
variable. Por tanto, es importante que en el lado derecho del operador de
asignación (:=) se encuentre un valor correspondiente al tipo de variable de
lado izquierdo.
Program Cuatro;
const
Calle = ’ Paloma Blanca ’;
48
CAPÍTULO 5. DATOS
Direccion = 11 ;
Puerta
= ’B’ ;
var
nombre : String[40];
numero : Integer;
letra : Char;
begin
{ Asignaciones incorrectas }
(* Asignaciones ilegales
{1} nombre := Direccion;
{2} numero := Calle;
{3} numero := Puerta;
{4} letra := Calle;
{5} letra := Direccion ;
{6} nombre := 98;
{7} numero := ’ Paloma Negra ’;
{8} numero := ’A’ ;
{9} letra := ’ Paloma Negra ’;
{10} letra := 97 ;
*)
(* Asignaciones legales *)
{11} nombre := Puerta;
{12} nombre := ’C’;
{ Asignaciones
{13} nombre
{14} nombre
{15} numero
{16} numero
{17} letra
{18} letra
end.
correctas }
:= Calle;
:= ’ Paloma Negra ’;
:= Direccion;
:= 97;
:= Puerta;
:= ’A’;
Las instrucciones {11} y {12} son incorrectas porque asignan a una variable
del tipo String una constante del tipo Char. No obstante, estas instrucciones no dan lugar a error en PASCAL porque el compilador interpreta las
variables Char como un subconjunto de las String (El espacio reservado en
5.7. ASIGNACIÓN DE LOS DISTINTOS TIPOS DE VARIABLES
49
memoria para un String acomoda perfectamente un Char ).
En el caso de valores numéricos,
Program Cinco;
const
Doce = 12.0;
Once = 11;
var
i : Integer;
z : Real;
begin
{ Asignaciones incorrectas }
(* Asignaciones ilegales
{1} i := Doce ;
{2} i := 24 ;
*)
(* Asignaciones legales *)
{3} z := Once ;
{4} z := 22
;
{ Asignaciones correctas }
{5} i := Once ;
{6} i := 24 ;
{7} z := Doce ;
{8} z := 22. ;
end.
el compilador interpreta los Integer como un subconjunto de los Real.
Para los efectos sintácticos, cualquier valor numérico ( o dato alfanumérico)
en el cuerpo del programa se considera igual que situado en el lado derecho
en una asignación de constante del tipo correspondiente.
50
CAPÍTULO 5. DATOS
Capı́tulo 6
Intercambio básico de datos y
resultados
6.1
Dispositivos de entrada, salida y almacenamiento
La comunicación entre el ordenador y el usuario se realiza a través de los
dispositivos denominados de entrada–salida (E/S , I/O). Todo programa de
ordenador se comunica con el exterior. Algunas veces la comunicación se
realiza directamente con el usuario a través del teclado y el terminal TRC.
Otras, se utilizan intermediarios como son los dispositivos de almacenamiento
de información (discos, diskettes,...). En este último caso, el programa lee los
datos del archivo donde se guardan y también puede escribir los resultados
en el archivo que se halla indicado para el almacenamiento.
Para cada problema concreto habrá un modo óptimo de comunicación
con el ordenador.
En aquellos casos en los que se utilicen exclusivamente dispositivos de
almacenamiento intermedio de información, se estará descartando la posibilidad de un proceso interactivo, es decir, la posibilidad de que el usuario
pueda utilizar la información generada por el ordenador para proporcionar
nuevos datos.
En este tema, se verán los modos básicos de intercambio de información
con el ordenador, dirigido desde un Programa PASCAL . Supondremos que
los dispositivos que se utilizan para entrada y salida de datos son el teclado y el terminal TRC, respectivamente. En muchos sistemas operativos el
51
52
CAPÍTULO 6. ENTRADA Y SALIDA
redireccionamiento necesario para utilizar archivos es trivial.
Las instrucciones de E/S se especifican dentro del cuerpo principal del
Programa PASCAL . Se trata de una acción que permite transmitir información desde el exterior a la memoria del ordenador, y viceversa. Obviamente,
el resultado de una instrucción de salida depende del contenido de la memoria
del ordenador en ese instante.
6.2
Las funciones Read y Write
Para la entrada de datos, PASCAL dispone del procedimiento básico Read(),
y para la salida de Write().
{1}
{2}
{3}
Program Uno;
{ Cuadrado de un numero entero }
Var
base , resultado : integer;
Begin
Read(base);
resultado := base * base;
Write(resultado);
End.
La instrucción {1} del programa Uno asigna a la variable base el valor
entero suministrado por el usuario. La introducción del dato se puede realizar tecleando el número deseado y pulsando después la tecla de retorno
de carro. El procedimiento Read(argumento) asigna a la variable argumento el conjunto de caracteres que se teclean antes del retorno de carro,
convenientemente interpretados de acuerdo con el tipo de dato de la variable
argumento.
La instrucción {3} escribe en la pantalla del terminal el resultado de
elevar al cuadrado el dato.
Si se teclea el número 93, se obtiene en pantalla 8649, pero si se teclea
93.0 el programa emitirá un mensaje de error. Esto es debido a que el
procedimiento Read incluye una verificación de que el dato suministrado
corresponde al tipo de dato definido para la variable.
El modo en que se han de suministrar los datos al programa para que
sean aceptados por el procedimiento Read() es muy parecido al modo en
6.2. LAS FUNCIONES READ Y WRITE
53
que se han de asignar los valores de las constantes dentro de un Programa
PASCAL . De hecho, es igual salvo dos excepciones:
• Los datos de las variables Char y String NO deben ir entre comillas
(’ ’ ).
• Los valores de variables Boolean no pueden leerse.
Tanto Read() como Write() admiten múltiples argumentos, separados
por comas.
{1}
{2}
{3}
Program Dos;
Var
resultado, base , altura : Integer;
Begin
Read(base,altura);
resultado := base * altura;
Write(’Area del cuadrado: ’,resultado);
End.
La instrucción {1} del programa Dos lee dos números enteros que asigna a
la variable base (el primero), y a altura (el segundo).
El modo en el que el usuario puede teclear los datos de entrada es bastante flexible. Por ejemplo en este caso, dado que son dos números enteros,
Read interpretará un entero como una secuencia de caracteres sin ningún
blanco intermedio. Supongamos que los datos son los números 27 y 31. La
secuencia de caracteres que teclea el usuario puede ser:
2
7
3
1
←
El procedimiento Read() interpreta los caracteres hasta el primer blanco
como integrantes del valor que se desea asignar al Integer base. Al segundo
argumento se asignará la cadena de caracteres que antecedan al siguiente
blanco o carácter de control < CR >. Una vez finalizada la ejecución de la
instrucción {1} estarán almacenados los valores 27 y 31 en las posiciones
de memoria asignadas a las variables base y altura, respectivamente.
El procedimiento Read() espera que se tecleen suficientes caracteres para
poder asignar valores a los argumentos de Read, y los caracteres de control
< CR > que se tecleen antes de cumplirse esta condición son irrelevantes. Por
54
CAPÍTULO 6. ENTRADA Y SALIDA
tanto, cualquiera de las dos secuencias siguientes de teclado son equivalentes
a la discutida antes.
2
7
←
←
3
1
←
←
2
7
←
3
1
←
El procedimiento Write escribe en el dispositivo de salida el contenido
de las constantes o las variables que se encuentran en el argumento.
• ’Area del cuadrado :’ es una constante del tipo String cuyo valor
es la ordenación de los 19 caracteres entre las comillas ’.
• resultado es una variable Integer, y el procedimiento Write() escribe su valor: 837.
La salida del programa Dos es :
Area del cuadrado: 837
Entre ambos valores el procedimiento Write() no escribe nada, y el entero,
el TURBO PASCAL lo escribe a partir de la primera cifra significativa. Otros
compiladores de PASCAL pueden hacerlo dejando algún blanco a la izquierda
de la primera cifra significativa.
6.3
Formatos
En un principio es deseable controlar exactamente el modo en el que se
escribe el valor de las variables en el dispositivo de salida. Esto puede hacerse especificando el formato de escritura. En PASCAL cualquier variable o
constante que se incluya en el argumento del procedimiento Write() puede
acompañarse de una especificación del formato de salida del siguiente modo:
nombre:n:m
donde nombre es el indicativo de la variable o constante, n es el número de
espacios que ha de ocupar cuando se muestra, y m el número de espacios
6.4. LAS FUNCIONES WRITELN Y READLN
55
que han de ocupar la parte decimal (después del punto) cuando el tipo de
variable o constante lo permita.
Por ejemplo,
Program Tres;
var
resultado, base , altura : Integer;
Begin
{1}
Read(base,altura);
{2}
resultado := base * altura;
{3}
Write(’Area del triangulo: ’:40,resultado:6);
end.
dará como resultado
Area del triangulo:
837
Los espacios que no ocupan el contenido de la variable o constante, se sitúan
a la izquierda. En el caso de tratarse de números reales, si no se especifica el
número de cifras decimales, se toma por defecto la notación cientı́fica. Estos
son algunos ejemplos de escritura de un número real:
Write(102030.40:8 )
Write(102030.40:10)
Write(102030.40:12)
Write(102030.40:14)
Write(102030.40:8:1)
Write(102030.40:10:1)
Write(102030.40:14:2)
6.4
1.0E+0005
1.0E+0005
1.020E+0005
1.02030E+0005
102030.4
102030.4
102030.40
Las funciones WriteLn y ReadLn
La forma que toma la representación de la salida externa de un Programa
PASCAL depende también de la ordenación en el sentido vertical. El carácter
de control < EOL > ( fin de lı́nea ) gobierna lo que serı́a el salto de carro
de una impresora. El PASCAL dispone del procedimiento Writeln() que es
equivalente al Write() salvo que después de escribir los argumentos también
escribe la secuencia de control < EOL >. Ası́, la salida del programa Cuatro
56
CAPÍTULO 6. ENTRADA Y SALIDA
Program Cuatro;
var
resultado, base , altura : Integer;
Begin
{1}
Read(base,altura);
{2}
resultado := base * altura;
{3}
Writeln(’ Resultado del calculo.’);
{4}
Writeln(’Area del triangulo: ’:40,resultado:6);
end.
para los mismos datos suministrados al programa Tres será:
Resultado del calculo.
Area del triangulo:
837
Equivalentemente, existe el procedimiento Readln() que lee el valor de sus
argumentos siempre y cuando no encuentre la secuencia de control < EOL >.
Al programa Cinco se han de introducir los datos de modo distinto que
al Cuatro. Si se utiliza el teclado, hay que teclear el carácter de control
< CR > entre los dos números, puesto que < CR > actúa también como
indicador de fin de linea.
Program Cinco;
var
resultado, base , altura : Integer;
Begin
{1}
Readln(base);
{2}
Readln(altura);
{3}
resultado := base * altura;
{4}
Writeln(’ Resultado del calculo.’);
{5}
Writeln(’Area del triangulo: ’:40,resultado:6);
end.
En un archivo, los saltos de carro están codificados con el carácter ASCII
correspondiente a la secuencia de control < EOL >.
Por ejemplo, un archivo que se imprime o aparece en pantalla del siguiente
modo:
Para vivir no quiero
islas, palacios, torres.
6.4. LAS FUNCIONES WRITELN Y READLN
57
está almacenado como:
Para vivir no quiero<EOL>islas, palacios, torres.<EOF>
donde < EOF > es la secuencia de control fin de fichero. El dispositivo
de salida pertinente se encargará de que aparezcan los dos versos en lı́neas
distintas.
El procedimiento Readln() utiliza < EOL > como delimitador de su
ámbito de lectura y lee los argumentos entre los caracteres que se encuentran
entre dos < EOL >. Por ejemplo, el resultado del programa Cinco, cuando
lee del fichero
3
4
es:
Resultado del calculo.
Area del triangulo:
12
mientras que si el fichero de entrada es:
3
5
4
la salida será:
Resultado del calculo.
Area del triangulo:
15
porque la instrucción {1} lee el 3 y omite el 4. La instrucción {2} lee el 5.
58
CAPÍTULO 6. ENTRADA Y SALIDA
Capı́tulo 7
Acciones
7.1
Operaciones básicas.
Una vez conocidos los modos básicos que existen en PASCAL para almacenar
datos en la memoria del ordenador y conocido el modo básico de comunicación con el usuario del programa, pasamos a estudiar qué operaciones se
pueden realizar con los datos.
7.1.1
Operadores aritméticos y expresiones aritméticas.
Cuando los datos con los que se realizan operaciones son números, lo más
común es realizar operaciones aritméticas : suma, resta, multiplicación, y
división. En PASCAL estas operaciones se especifican con los operadores +
, – , * , y / , respectivamente.
Las instrucciones,
a:=
b:=
b:=
c:=
3. + 5.;
a - 2.;
b * a * 6.;
-b / 12.;
son ejemplos de la utilización de los operadores. En el caso más sencillo
de asignación con dos operandos solamente, no hay ambigüedad y con este
59
60
CAPÍTULO 7. ACCIONES
formato de instrucción,
variable1 := operando1


+ 




 − 


∗ 






operando2;
/
se asigna a la variable variable1 el resultado de la operación aritmética correspondiente con las convenciones usuales de la aritmética. Tanto operando1
como operando2, pueden ser constantes o variables. Cuando se desea operar
con más de dos datos hay que especificar claramente y sin ambigüedad el
orden en que se quieren realizar las operaciones. Para ello, podemos ayudarnos de los paréntesis con el mismo fin con el que se usan en las expresiones
aritméticas. Por ejemplo, la siguiente instrucción
b:= ( -37.0 * (a - 2.) ) / (a + 5.);
carece de ambigüedad. No obstante, el PASCAL tiene una prioridad establecida para el orden de las operaciones:
1. más y menos monarios.
2. paréntesis.
3. multiplicación y división.
4. suma y resta.
Las siguientes instrucciones son equivalentes:
b:= -27.0 * c + 35. / a + b + c;
b:= ( (-27.0) * c ) + ( 35. / a ) + b + c ;
b:= ( -27.0 * c ) + ( 35. / a ) + b + c ;
y la forma concreta en la que el programador debe escribirla es aquella que
resulte menos confusa. Un error común entre principiantes es escribir la
instrucción {i} queriendo escribir la {ii} :
{i}
{ii}
{iii}
b:= a * 27.0 / b + c ;
b:= a * 27.0 / ( b + c ) ;
b:= ( a * 27.0 / b ) + c ;
7.1. OPERACIONES BÁSICAS.
7.1.2
61
Funciones aritméticas.
En PASCAL están definidas funciones que permiten realizar cálculos matemáticos muy frecuentes u operaciones numéricas muy útiles para transformar expresiones aritméticas en sentencias PASCAL . En PASCAL las
funciones son llamadas a procedimientos de cálculo que devuelven un valor.
Si aparecen en una expresión aritmética, las operación se realiza con el resultado de evaluar la función. Si suponemos que se utiliza la función para una
asignación,
y :=
F ( x ) ;
los tipos de datos que admiten como argumento, x, los tipos de datos que
proporcionan, y, y las funciones que realizan son:
F(x)
x
y
Abs ( x )
Real o Integer igual que x
Valor absoluto
Arctan ( x ) Real o Integer
Real
Arco tangente
Cos ( x )
Real o Integer
Real
Coseno
Exp ( x )
Real o Integer
Real
Exponenciacion ( ex )
Frac ( x )
Real o Integer
Real
La parte decimal de x
Int ( x )
Real o Integer
Real
La parte entera de x
Ln ( x )
Real o Integer
Real
Logaritmo neperiano
Pred ( x )
Integer
Integer
x−1
Random
Real
Número aleatorio, y[0, 1]
Random(x)
Integer
Integer
Número aleatorio, y[0, x]
Round ( x )
Real
Integer
x redondeado al entero más cercano
Sin ( x )
Real o Integer
Real
Seno
Sqr ( x )
Real o Integer
Real
Exponenciacion
( x2 )
√
Sqrt( x )
Real o Integer
Real
x
Succ ( x )
Integer
Integer
x+1
Trunc ( x )
Real
Integer
x con la parte decimal eliminada
Las funciones trigonométricas que no están incluidas, se calculan fácilmente
a partir estas. Por ejemplo,
tan1 := Sin(x) / Cos(x);
a := Sin(x);
tan2 := a / Sqrt ( 1.0 - a * a ) ;
62
7.1.3
CAPÍTULO 7. ACCIONES
Aritmética entera y real.
El PASCAL realiza una comprobación escrupulosa del tipo de los datos en
general, y en particular en el procesamiento de las expresiones aritméticas.
Cuando se programan estas expresiones hay que verificar que se mezclan
coherentemente datos reales y enteros.
PASCAL considera Integer el resultado de una expresión aritmética
siempre que en la expresión aparezcan sólo datos de este tipo y no esté
implicada una división. El el programa Uno,
Program Uno;
Var
i,j : Integer;
z : Real;
Begin
{1}
j := 8;
(*
{2}
i := j / 4;
Expresion aritmetica invalida
*)
{3}
z := j / 4;
{4}
Writeln(z:4:2);
end.
la instrucción {2} es incorrecta, si bien la salida es 2.00 . Por defecto, el
resultado de una expresión aritmética que no es Integer , es Real . Basta
que haya un solo operando Real o un operador división en una expresión
para que el dato que resulte de la operación sea real y que toda la expresión
se evalúe como si todos sus componentes fueran reales.
Cuando se trata de hacer aritmética de números enteros hay que llevar
mucho cuidado con el lı́mite de capacidad del tipo de dato que se utilice.
Algunas veces, no es trivial. La salida del programa Dos ,
Program Dos;
Var
i,j :
z
:
Begin
{1}
{2}
Integer;
Real;
j := 2333;
i := j * 23;
7.1. OPERACIONES BÁSICAS.
{3}
(*
{4}
*)
{5}
{6}
{7}
{8}
{9}
{10}
end.
63
Writeln(i:9);
i := 2333 * 23;
Expresion aritmetica invalida
z := j * 23;
Writeln(z:9:0);
z := 1.0 * j * 23 ;
Writeln(z:9:0);
z := 2333 * 23;
Writeln(z:9:0);
es:
-11877
-11877
53659
53659
La instrucción {2} es aparentemente correcta, pero como el resultado de
la multiplicación excede la capacidad de un Integer (32767), el resultado
de la operación es un número entero, pero absurdo. Sin embargo, la {4} es
claramente incorrecta y el compilador PASCAL lo detecta al intentar almacenar en el lugar de memoria reservado para i el producto, en vez de generar
el código para realizar la multiplicación durante la ejecución.
La {5} es sólo aparentemente correcta, puesto que es similar a la {2}. Si
bien el resultado de la operación se quiere almacenar en una variable Real , el
compilador interpreta que la expresión aritmética se puede realizar con datos
enteros y genera código tal que los resultados intermedios se almacenarán en
registros de 2 bytes. El resultado : un desastre, el dato que se almacena
finalmente en la variable real es idéntico al anterior. Sin embargo, si en la
expresión se incluye cualquier operando Real , el compilador asume que se
trata de una operación que ha de realizarse con aritmética real, y utilizará
registros apropiados para esa aritmética. La instrucción {7} es correcta, y
solo difiere de la {5} en la multiplicación por 1.0, algo matemáticamente
irrelevante pero no computacionalmente. Hay que tener siempre presente
que el modo con el que se van a realizar las operaciones de una expresión
64
CAPÍTULO 7. ACCIONES
aritmética no está determinado por el lado izquierdo de la asignación ( el
tipo de dato que acepta la variable), sino por los operandos.
Cuando aparecen constantes numéricas en una expresión aritmética en la
que se quiere realizar un cálculo con números reales, es aconsejable no olvidar
el punto que lo identifica como constante real. Si i es una variable Integer
el resultado de estas dos expresiones será distinto para i > 9.
z := 3600 * i ;
z := 3600. * i ;
Cuando se quieren realizar divisiones respetando la aritmética de enteros
hay que utilizar el operando Div. El resultado de
j
Div
i
es j/i cuando j es múltiplo de i, y la parte de entera de j/i en el caso
contrario.
En el programa Tres ,
Program Tres;
Var
i,j,k : Integer;
Begin
{1}
i := 11; j := 6;
{2}
k :=10 * ( (i+j) Div j );
{3}
Writeln(k:9);
{4}
k := 10 * Trunc ( (i+j) / j );
{5}
Writeln(k:9);
{6}
k := Trunc (10* ( (i+j) / j ) );
{7}
Writeln(k:9);
end.
las instrucciones {2} y {4} generan el mismo resultado, pero distinto al de la
{6}. La expresión aritmética en {6} es el argumento de la función Trunc()
y la presencia del operador / indica que se ha de realizar aritmética de
números reales.
Cuando se quieren utilizar datos Real en expresiones con aritmética de
números enteros, lo más apropiado es utilizar la funciones de transformación
de tipo, Round() o Trunc() , para homogeneizar el tipo de los operandos.
7.1. OPERACIONES BÁSICAS.
65
Existe otro operador especı́fico de números enteros que es el operador
mod y proporciona el resto de la división entera. El resultado de
j
M od i
es 0 cuando j es múltiplo de i, y el resto de la división j/i en el caso contrario.
Program Cuatro;
Var
i,j,k,m : Integer;
Begin
{1}
i := 11; j := 6;
{2}
k :=j mod i;
{3}
Writeln(k:9);
{4}
m := j * (i mod j) + k;
{5}
Writeln(i:9,’ ’,m:9);
end.
La salida del programa Cuatro es:
2
14
14
dado que en la instrucción {4} se recupera para m el valor original de i .
7.1.4
Operadores lógicos.
Los operadores lógicos que proporciona el PASCAL son de dos tipos:
1. Operadores que actuando sobre datos no-Boolean proporcionando un
dato Boolean.
2. Operadores que actuando sobre datos Boolean proporcionan un dato
Boolean.
El primero de estos tipos es muy útil para tomar decisiones como resultado
de la comparación de variables. La forma general en la que aparecen estos
66
CAPÍTULO 7. ACCIONES
operadores es:
a


= 








<> 





 > 


< 










<=






b
>=
donde a y b pueden ser números reales , enteros o caracteres, mientras que
el resultado de la operación es un dato Boolean (FALSE o TRUE ). La resstricción obvia es que a y b han de ser constantes o variables del mismo
tipo.
El resultado de la operación será TRUE en los siguientes casos:
Operador Condición
=
a=b
<>
a 6= b
>
a>b
<
a<b
<=
a≤b
>=
a≥b
En el caso de que de que a y b sean caracteres, las operaciones de comparación
se realizan sobre el valor entero del carácter ASCII correspondiente.
Ejemplos de utilización de estos operadores son:
Program Cinco;
Var
quizas
: Boolean;
letra
: Char;
i,j
: Integer;
a,tan1,tan2,x
: Real;
Begin
{1}
i := -11; j := 6; letra := ’h’; x:= 1.3;
{2}
quizas := i > j;
{3}
Writeln(quizas);
(* FALSE *)
{4}
quizas := ’k’ <= letra ;
{5}
Writeln(quizas);
(* FALSE *)
{6}
tan1 := Sin(x) / Cos(x);
7.1. OPERACIONES BÁSICAS.
{7}
{8}
{10}
{11}
end.
67
a := Sin(x);
tan2 := a / Sqrt ( 1.0 - a * a ) ;
quizas := ( tan1 = tan2 );
Writeln(quizas);
(* FALSE *)
Merece la pena recordar una vez más que no se pueden trasladar directamente las expresiones matemáticas a las sentencias de un programa. Por
ejemplo, en la instrucción {10} se asigna a quizás el valor FALSE , si bien
las expresiones utilizadas en las instrucciones {6} y {8} para calcular la
tangente son matemáticamente equivalentes. Debido a la utilización de una
representación finita de los números reales ambos valores serán ligeramente
distintos. La comparación para averiguar la similitud de números reales se
suele hacer utilizando la diferencia relativa:
quizas :=
Abs ( (tan1 - tan2) / tan1 )
<=
0.000001;
Con el segundo tipo de operadores lógicos se pueden combinar datos Boolean
para realizar operaciones lógicas complejas. Uno de ellos es monario y se
utiliza según la forma general:
Not
operando
donde tanto operando como el resultado de la operación son datos del tipo
Boolean. El resultado, es la negación del operando:
a
Not a
True False
False True
El resto de los operadores se utilizan según la forma:
A



 And 



Or

Xor 
donde A y B son variables Boolean.
Las tablas de verdad son las habituales:
B
68
CAPÍTULO 7. ACCIONES
A
B
True False
False True
False False
True True
A
B
True False
False True
True True
False False
A
B
True False
False True
True True
False False
7.1.5
A And B
False
False
False
True
A Or B
True
True
True
False
A Xor B
True
True
False
False
Expresiones lógicas.
Denominamos Expresiones lógicas a las operaciones que se realizan con datos Boolean. Estas operaciones pueden encadenarse de un modo bastante
complejo. Un ejemplo de utilización de estas expresiones es el siguiente:
Supongamos que tenemos que escribir un programa para determinar si
un punto se encuentra en la intersección de dos rectángulos,
7.1. OPERACIONES BÁSICAS.
69
B
A
y también queremos saber si forma parte de la frontera que la delimita. El
programa Seis proporciona la respuesta utilizando expresiones lógicas:
Program Seis;
Const
(* Coordenadas cartesianas que definen los rectángulos *)
xa1 = 2; ya1 = 2; { Esquina inferior de A }
xa2 = 7; ya2 = 6; { Esquina superior de A }
xb1 = 5; yb1 = 4; { Esquina inferior de B }
xb2 = 10; yb2 = 8; { Esquina superior de B }
var
dentro1, dentro2, dentrox , dentroy , frontera : Boolean;
puntox,puntoy : Integer;
Begin
{1}
Readln(puntox , puntoy); {Coordenadas del punto problema}
(* Sin considerar la frontera *)
{2}
dentrox := ( puntox > xb1 ) And (puntox < xa2);
{3}
dentroy := ( puntoy > yb1 ) And (puntoy < ya2);
{4}
dentro1 := dentrox And dentroy;
{5}
Writeln(’ Si no se considera el borde : ’,dentro1);
(* Considerando la frontera *)
{6}
dentro2 := ( puntox >= xb1 ) And (puntox <= xa2) And
70
{7}
{8}
{9}
end.
CAPÍTULO 7. ACCIONES
( puntoy >= yb1 ) And (puntoy <= ya2);
Writeln(’ Si se considera el borde :
’,dentro2);
(* Condicion de frontera *)
frontera := dentro2 And ( Not dentro1);
Writeln(’ Frontera: ’,frontera);
Las instrucciones {2} y {3} se podrı́an haber condensado en una sola instrucción similar a la {6}.
Cuando en una expresión se combinan operadores lógicos y aritméticos
hay que utilizar generosamente los paréntesis o llevar mucho cuidado con el
orden de prioridad establecido por el PASCAL para la aplicación de operadores:
Los operadores aritméticos se aplican siempre antes que los lógicos.
Por ejemplo, la expresión
(37 * 4
> 5 * 24) And
Not (65 < 2 * 31)
se evalúa a True y los paréntesis son estrictamente necesarios. De no existir
el primer juego de paréntesis el operador And tendrı́a a su izquierda un dato
Integer y a su derecha un dato Boolean, lo que es incorrecto. De no
existir el segundo juego de paréntesis habrı́a también una disparidad de tipos
de datos algo más compleja que se analizará más adelante.
Como en el caso de los operadores aritméticos, en los lógicos también los
monarios se aplican primero, y en esta expresión el operador Not se aplica
antes que el And.
El uso generoso de los paréntesis en las expresiones que mezclan operadores lógicos y aritméticos está recomendado por otra razón más:
Los operadores And, Or , y Xor pueden ser aritméticos o lógicos
según el tipo de operandos sobre los que actúen.
Esto es ası́ porque como veremos a continuación también están pensados para
realizar manipulación de bits.
7.1.6
Manipulación de bits.
Existen operadores en PASCAL que permiten modificar directamente los
bits que componen los datos enteros. Estos datos están almacenados en la
7.1. OPERACIONES BÁSICAS.
71
memoria del ordenador con su valor binario, y los operadores de manipulación
de bits permiten al programador modificar directamente los componentes de
la representación binaria.
En una expresión



 And 

A


Or

Xor 
B
donde A y B son datos enteros ( tipo Integer, Byte, ShortInt, Word
y LongInt ), los operadores And, Or y Xor, actúan como operadores
aritméticos. El resultado de la operación es otro dato entero que se obtiene
realizando la operación lógica correspondiente entre cada uno de los bits de
los datos, considerando el 1 como verdadero y el 0 como falso.
Un dato Byte esta representado en memoria por 8 bits correspondientes
a la representación binaria del número estando el bit más significativo a la
izquierda. Por ejemplo, la representación interna del dato Byte 77 es:
0
1
0
0
1
1
0
1
1
1
1
1
0
y la del dato Byte 62 es :
0
0
1
La operación And entre estos dos datos da como resultado el dato Byte :
0
0
0
0
1
1
0
0
cuyo valor decimal es 12. Si sobre los datos originales se realiza la operación
Or el resultado es:
0
1
1
1
1
1
1
1
cuyo valor decimal es 127, y si se realiza la operación Xor el resultado es:
0
1
1
1
0
0
1
1
72
CAPÍTULO 7. ACCIONES
cuyo valor decimal es 115. Estas tres operaciones son las que se realizan en
el programa Siete :
Program Siete;
Var
a , b , c: Byte;
Begin
a := 77;
b := 62;
c := a And b;
Writeln(c);
c := a Or b;
Writeln(c);
c := a Xor b;
Writeln(c);
End.
Existen otros dos operadores de manipulación de bits : Shl (Shift left) y
Shr (Shift right). El resultado de
(
A
Shl
Shr
)
n
donde A y n son enteros, es otro entero que resulta de desplazar n bits de A
hacia la izquierda (Shl ) o hacia la derecha (Shr ). Los bits que se introducen
en el desplazamiento son nulos. Por ejemplo, si el dato A es,
a1
a2
a3
a4
a5
a6
a7
a8
0
0
0
B, el resultado de
B :=
A Shl 3;
es,
a4
a5
a6
a7
a8
y, C, el resultado de
C :=
B Shr 7;
7.1. OPERACIONES BÁSICAS.
73
es,
0
0
0
0
0
0
0
a4
Como resultado de la concatenación de estas dos operaciones hemos obtenido
un entero cuyo valor es el del quinto bit, empezando por la derecha del entero
original.
El programa Ocho escribe un 1 si el numero que recibe (un entero entre
0 y 255) es impar, y cero de lo contrario:
Program Ocho;
Var
a , b , c: Byte;
Begin
Readln(a);
b := a Shl 7;
c := b Shr 7;
Writeln(c);
End.
En el caso de los datos Integer ( 2 bytes), que permiten almacenar
números enteros con signo, el valor está almacenado en memoria de un modo
más complejo. El bit más a la izquierda de todo almacena el signo: 1 si es
negativo y 0 de lo contrario. El valor del entero está almacenado en los 15
bits restantes del modo siguiente:
• Positivos: El valor binario del entero con el bit menos significativo a la
derecha.
• Negativos: Igual que si fuera positivo pero considerando el valor que
resulta de sumar al entero 32768.
Por ejemplo, -32768 se almacena como:
1000000000000000
y -1 como:
1111111111111111
mientras que 1 se almacena como
0000000000000001
74
CAPÍTULO 7. ACCIONES
y el 32767 como
0111111111111111.
7.2
Sentencias de control.
La mayorı́a de los procedimientos que podemos diseñar para resolver problemas incluyen la elección de uno entre los casos posibles en función del valor
particular de alguno de los datos. Las sentencias de control en PASCAL son
la condicional simple If y la condicional múltiple Case.
La sentencia de control simple tiene la siguiente sintaxis:
If datoBoolean
Then
accion1
o bien,
If datoBoolean
Then
accion1
Else
accion2
donde datoBoolean es una variable Boolean o una operación cuyo resultado
es una dato Boolean, accion1 y accion2 son instrucciones PASCAL que
pueden ser tanto simples como compuestas.
El efecto de esta sentencia de control es el siguiente: si datoBoolean es
True se procede a realizar la instrucción accion1; en el caso de que sea False
y esté presente la parte Else, se procede a realizar la instrucción accion2.
En el primero de los casos el esquema de la bifurcación serı́a:
7.2. SENTENCIAS DE CONTROL.
75
Boolean
?
HH
? H
b
b ""
b"
Cierto?
accion1
Falso
?
?
Continuar
y en el segundo caso, cuando está presente la parte Else de la estructura:
Boolean
?
a
a
"
H
H?
?
?
accion2
-
accion1
Continuar ?
En el programa Nueve tenemos un ejemplo de utilización de esta construcción. Ambas son equivalentes: en la primera se realiza la comparación en la
construcción If, mientras que en la segunda se utiliza una variable Boolean
auxiliar.
Program Nueve;
Var
positivo : Boolean;
n : Integer;
Begin
Readln (n);
If n >= 0
76
CAPÍTULO 7. ACCIONES
Then
Writeln(’ Se trata
Else
Writeln(’ Se trata
positivo := ( n >= 0 ) ;
If positivo
Then
Writeln(’ Se trata
Else
Writeln(’ Se trata
de un numero positivo ’)
de un numero negativo ’);
de un numero positivo ’)
de un numero negativo ’);
End.
Las acciones que se ejecutan después de la comprobación del dato Boolean
pueden ser instrucciones sencillas o compuestas. En el programa Diez podemos ver un ejemplo de utilización con una instrucción compuesta:
Program Diez;
Var
x , y : Real;
Begin { Diez }
{ Programa que calcula la raiz cuadrada de un numero }
Writeln(’ Calculo de la raiz cuadrada. Escriba el numero’);
Readln(x);
If x > 0.0 then
begin
Writeln(’ Raiz cuadrada: ’);
y := Sqrt(x);
Write(y)
end
Else
Writeln(’ Numero negativo.’);
End. { Diez }
Con esta estructura If ... Then ... Else también se pueden realizar decisiones múltiples. Un modo muy frecuente de utilización es la encadenación
de estructuras de la forma:
7.2. SENTENCIAS DE CONTROL.
77
If datoBoolean1
Then
accion1
Else
If datoBoolean2
Then
accion2
. . .
En el programa Once se utiliza una concatenación de estructuras para realizar una elección múltiple.
Program Once;
Var
n : Integer;
Begin { Once}
{ Programa que reescribe con letras un numero del 1 al 5 }
Writeln(’ Teclear un numero del 1 al 5’);
Readln(n);
If n = 1 then
Writeln(’ Uno ’)
Else If n = 2 Then
Writeln(’ Dos ’)
Else If n = 3 Then
Writeln(’ Tres ’)
Else If n = 4 Then
Writeln(’ Cuatro ’)
Else If n = 5 Then
Writeln(’ Cinco ’)
Else
Writeln(’ Numero fuera de rango’);
End. { Once }
Este tipo de bifurcación múltiple puede llegar a ser difı́cil de leer y el
PASCAL proporciona una estructura más legible para aquellos casos en los
que la bifurcación múltiple se realiza mediante comparación de datos sencillos
ordinales (por tanto ni Real, ni String). Esta es la estructura Case que
tiene la siguiente sintaxis:
78
CAPÍTULO 7. ACCIONES
Case variable Of
caso1 : accion1
caso2 : accion2
. . .
End;
o bien,
Case identificador Of
caso1 : accion1
caso2 : accion2
. . .
Else
acciond
End;
donde identificador es una variable del tipo Integer, Byte, o Char, las
acciones accion1,... son instrucciones sencillas o compuestas, y los casos
caso1, ... son la especificación de los valores. Esta especificación se puede
hacer separando valores con comas o bien mediante el rango, si se trata de
valores consecutivos. Un rango se especifica mediante el valor inicial y final
separados por dos puntos. El programa Doce es equivalente al Once pero
mucho más legible y eficiente.
Program Doce;
Var
n : Integer;
Begin { Doce}
{ Programa que reescribe con letras un numero del 1 al 5 }
Writeln(’ Teclear un numero del 1 al 5’);
Readln(n);
Case n
1 :
2 :
3 :
4 :
5 :
Else
Of
Writeln(’
Writeln(’
Writeln(’
Writeln(’
Writeln(’
Uno ’);
Dos ’);
Tres ’);
Cuatro ’);
Cinco ’);
7.3. SENTENCIAS DE REPETICIÓN.
79
Writeln(’ Numero fuera de rango’);
End;
End. { Doce }
Un uso más complejo de la estructura Case se puede ver en el siguiente
ejemplo:
Program Trece;
Var
c : Char;
n : Byte;
Begin { Trece}
{ Programa que identifica un caracter }
Writeln(’ Teclear el caracter’);
Readln(c);
Case c Of
’a’..’z’, ’A’..’Z’ :
Writeln(’Letra’);
’0’..’9’
:
Writeln(’Numero’);
’*’,’/’,’+’,’-’
:
Writeln(’Operador aritmetico’);
’>’,’<’,’=’
:
Writeln(’Operador logico’);
Else
Begin
n := Ord(c);
Writeln(’Simbolo ASCII ’,n)
end;
End;
End. { Trece }
donde la función intrı́nseca del PASCAL Ord() es una función que devuelve
el valor ordinal de cualquier argumento escalar, incluyendo los Char.
7.3
Sentencias de repetición.
Hay quien opina que la programación es una actividad que nunca puede resultar tediosa o repetitiva, ya que todo aquello que ha de hacerse repetidamente
80
CAPÍTULO 7. ACCIONES
puede programarse en una instrucción simple para que sea el ordenador quien
lo haga repetidas veces. De hecho, las sentencias de repetición son generalmente las responsables de la utilidad del ordenador. Se trata de instrucciones
que gobiernan la realización de tareas repetitivas mientras no sea cierta la
condición que se imponga para la finalización.
En PASCAL existen tres estructuras de repetición que son las construcciones For – Do, Repeat – Until, y While – Do.
La primera de ellas, la construcción For – Do, tiene la siguiente sintaxis:
For identificador := principio To final Do
accion
donde identificador es una variable que puede almacenar un dato ordinal,
principio es el valor que se le asigna a esa variable antes de realizar la acción,
final es el valor máximo que puede alcanzar la variable antes de finalizar la
acción, y accion es la instrucción, simple o compuesta, del PASCAL que se
repetirá incrementando en uno cada vez el valor de identificador. Si se trata
de una sentencia compuesta, tendrá que utilizarse la estructura Begin-End
para identificar el principio y el fin.
Existe también la opción equivalente
For identificador := principio Downto final Do
accion
en la que el valor del identificador se decrementa en uno cada vez que se
realiza la acción.
En el programa Catorce tenemos la aplicación de esta estructura para
el cálculo del factorial.
Program Catorce;
Var
z : Real;
n ,i : Integer;
Begin
Writeln(’Calculo del factorial (introducir el numero)’);
Readln(z);
n := Round(z);
If n > 33
7.3. SENTENCIAS DE REPETICIÓN.
81
Then
Writeln(’ Solo se puede calcular hasta 33!’)
Else
Begin
z := 1.;
For i:=1 to n do
z:= z * i;
Writeln(’El Factorial de ’,n:2,’ es : ’);
Writeln(z:40:0)
End;
End.
La limitación básica de la estructura For-Do es que la acción que implı́citamente
se realiza cada vez es simplemente el aumento o decremento de una variable ordinal. No obstante, hay que tener presente que variables ordinales no
son solamente los Integer. En el programa Quince vemos como se puede
utilizar la estructura con una variable Char.
Program Quince;
Var
i : Char;
Begin
(* Programa que escribe el alfabeto al reves *)
Writeln(’’);
For i:=’Z’ Downto ’A’ do
Write(’ ’,i);
End.
La variable que se utiliza como contador en la estructura For-Do puede
modificarse también en la acción que se repite, pero hay que ser extremadamente cuidadoso porque de lo contrario podemos generar un bucle infinito.
Si en el siguiente programa no se realizara la modificación a un múltiplo de
tres, se podrı́a caer en un bucle infinito (con probabilidad 0.33333).
Program Dieciseis;
Var
82
CAPÍTULO 7. ACCIONES
i,n : Integer;
Begin
Writeln(’Escribo los numeros de dos en dos’);
Writeln(’Empezando en 1 y acabando en ?’);
Readln(n);
n := n - (n Mod 3) ;
For i:=1 to n do
Begin
Writeln(i);
i := i +2
End;
End.
Aceptando que el programa anterior es siempre correcto, es fácil deducir
si la comprobación sobre el estado del ı́ndice se realiza antes o después de
incrementar el ı́ndice.
Una estructura muy similar a la For-Do pero más versátil es la RepeatUntil. Su sintaxis es:
Repeat
accion1;
accion2;
. . .
Until DatoBoolean
y con ella se especifica que se repita el conjunto de acciones situado entre
el Repeat y el Until mientras que no sea True el valor de DatoBoolean .
Las acciones se realizan al menos una vez porque la verificación sobre el dato
Boolean se hace después que las acciones. Ası́, en el siguiente programa
el juego no se interrumpe inmediatamente después de teclear el 0. Por el
contrario la jugada se repite y luego se termina.
Program Diecisiete;
Var
n ,i : Integer;
Begin
Writeln(’Adivina el numero que he generado ( del 1 al 9 )’);
Writeln(’ Teclea 0 para terminar ’);
7.3. SENTENCIAS DE REPETICIÓN.
83
Randomize;
Repeat
i := Random(8);
Writeln(’Dime un numero ’);
Readln(n);
If i+1 <> n
Then
Writeln(’ No acertaste, era el ’,i+1)
Else
Writeln(’ Si ’);
Until n = 0 ;
End.
Sin embargo, en la estructura While-Do se realiza la comprobación de
la condición que ha de finalizar la repetición antes de ejecutarse la acción.
Su sintaxis es:
While DatoBoolean Do
accion
donde DatoBoolean es una expresión cuyo resultado es un dato Boolean,
y accion es una instrucción PASCAL (sencilla o compuesta) que se repetirá
mientras DatoBoolean se evalúe a True.
El programa Dieciocho es una modificación del Diecisiete en el que
con el uso de la estructura While-Do se verifica la condición de fin del juego
antes de sortear el número.
Program Dieciocho;
Var
n ,i : Integer;
Begin
Writeln(’Adivina el numero que he generado ( del 1 al 9 )’);
Writeln(’ Teclea 0 para terminar ’);
Randomize;
Writeln(’Dime un numero ’);
Readln(n);
While n <>0 do
84
CAPÍTULO 7. ACCIONES
Begin
i := Random(8);
If i+1 <> n
Then
Writeln(’ No acertaste, era el ’,i+1)
Else
Writeln(’ Si ’);
Writeln(’Dime un numero ’);
Readln(n);
End; {endwhile}
End.
Cuando se ha de programar un procedimiento repetitivo, la elección de una
de las tres estructuras se hará siempre considerando la claridad y facilidad
de programación.
En PASCAL también existe la sentencia de control no estructurado Goto,
pero no la consideraremos pues en este curso se pretende que el alumno
ejercite la programación estructurada. Sin embargo, si que consideramos la
función Exit cuya efecto es interrumpir la ejecución del bloque en la que
se encuentra. Si se trata del programa principal, producirá la interrupción
de la ejecución del programa. En el programa Diecinueve se modifica el
Diecisiete para interrumpir el juego haciendo uso de la función Exit.
Program Diecinueve;
Var
n ,i : Integer;
Begin
Writeln(’Adivina el numero que he generado ( del 1 al 9 )’);
Writeln(’ Teclea 0 para terminar ’);
Randomize;
Repeat
i := Random(8);
Writeln(’Dime un numero ’);
Readln(n);
If n = 0 Then Exit;
If i+1 <> n
Then
Writeln(’ No acertaste, era el ’,i+1)
7.4. MANIPULACIÓN DE LOS DATOS STRING.
85
Else
Writeln(’ Si ’);
Until n = 0 ;
End.
7.4
Manipulación de los datos STRING.
Las sentencias de repetición son útiles para manipular el contenido de los datos String accediendo a cada uno de los caracteres que componen la cadena.
Como vimos en su dı́a, si s es un dato String ( una ordenación consecutiva
de datos del tipo Char) el carácter almacenado en s[0] indica el número
de caracteres almacenados en el dato. Ese carácter, como cualquier otro, se
puede convertir a dato entero utilizando la función Ord(). Igualmente, se
puede modificar la longitud asignando un nuevo carácter a s[0]. Los caracteres almacenados en s también se pueden modificar uno a uno y se accede
a ellos mediante s[i] donde i es un entero que corresponde a su número de
orden dentro de la cadena. Por ejemplo,
s[2] := ’b’;
hace que el segundo carácter almacenado en s sea el carácter ’b’.
En el programa Veinte tenemos un ejemplo sencillo de manipulación de
los caracteres de una cadena:
Program Veinte;
Var
s : String[10];
longitud, i : Integer;
Begin
s := ’ABCDEF’;
longitud := Ord(s[0]) ;
Writeln(’La longitud original es : ’,longitud);
For i:=1 to longitud Do
writeln(’Componente ’,i:2,’ : ’,s[i]);
s[0]:= Char(3);
s[2] := ’b’;
Writeln(’Despues de truncada y modificada: ’, s);
End.
86
CAPÍTULO 7. ACCIONES
y la salida que produce es:
La longitud original es : 6
Componente 1 : A
Componente 2 : B
Componente 3 : C
Componente 4 : D
Componente 5 : E
Componente 6 : F
Despues de truncada y modificada: AbC
Capı́tulo 8
Modularidad
8.1
Dividir para vencer
Con las estructuras vistas hasta ahora del PASCAL se podrı́an construir
programas que realizaran tareas muy complejas pero con escasa legibilidad.
Un modo muy común, y generalmente muy efectivo, de resolver un problema
es desmenuzarlo en una serie de problemas más sencillos y resolver después
cada uno de ellos. Es la estrategia que ha recibido el nombre dividir y vencer
o diseño Top-Down. Desde el punto de vista de la legibilidad es deseable
que un programa de ordenador pueda estructurarse siguiendo lo más posible
el esquema conceptual que se ha pensado utilizar para resolver el problema.
Por ejemplo, supongamos que se trata de escribir un programa que calcule
la suma de los n primeros términos de una sucesión geométrica de razón r y
cuyo primer término es a. Para ello pensamos utilizar la expresión:
n−1
X
i=0
ari =
a(1 − rn )
1−r
y el esquema que podemos trazar para el programa es el siguiente:
Suma de la progresión:
(1) Lectura de datos
(2) Cálculo de la expresión
(3) Salida de resultados
A su vez, el cálculo de la expresión matemática no es inmediato puesto que
es necesario calcular rn donde r es real y n entero. También podemos descomponer la tarea (2) en:
87
88
CAPÍTULO 8. MODULARIDAD
(2) Cálculo de la expresión
(2.1) Obtener rn
(2.2) Realizar multiplicaciones y divisiones.
A través de la descomposición modular que permite el PASCAL con las
construcciones Procedure y Function es posible escribir el programa de
un modo muy similar al pseudo código anterior. El cuerpo principal del
programa será simplemente:
Program SumaProgresion;
Var
n : Integer;
r,a,suma : Real;
(* Declaracion de procedimientos y funciones *)
. . . . . . . . . . . . . . . . . . . .
(* Fin de la parte declarativa *)
Begin { SumaProgesion }
{ Se utiliza la expresion:
suma = a (1-r**n) / (1-r) }
LeerDatos(n,a,r);
CalcularExpresion;
SacarResultados(suma);
End. { SumaProgresion}
y en el espacio reservado para declaraciones en el programa se especificará
qué tareas se han de realizar en los procedimientos LeerDatos, CalcularExpresion, y SacarResultados, definidos por el programador.
8.2
Procedimientos
La sintaxis con la que se especifican estos procedimientos es similar a la de
un programa PASCAL pero se usa la palabra reservada Procedure para
indicar que se trata de un procedimiento parcial:
Procedure nombre ( argumentos ) ;
8.2. PROCEDIMIENTOS
89
Parte declarativa
Begin
Bloque de sentencias del procedimiento
End;
La parte declarativa del procedimiento tiene el mismo fin que en un programa y en ella se definen tipos de datos, variables y constantes como en un
programa y también, si ası́ se requiere, procedimientos y funciones que sean
necesarios para la realización de este Procedure. En el bloque principal
del procedimiento se escriben las sentencias PASCAL que constituyen el algoritmo que ha de realizar, y en él se pueden utilizar datos definidos en su
parte declarativa, en la parte declarativa del programa principal o bien que
aparecen como argumentos del procedimiento. Estos argumentos se utilizan
para comunicar valores de datos al procedimiento desde el bloque donde ha
sido llamado. Cuando se llama al procedimiento, estos argumentos van separados por comas. En la declaración del procedimiento los argumentos se
especifican de un modo muy parecido a como se especifica el tipo de datos
en la declaración de las variables de un programa. Los datos de un mismo
tipo van separados por coma y se utiliza el punto y coma para separar datos
de distinto tipo. A continuación de los argumentos se especifica el tipo de
variable. Por ejemplo, al procedimiento SacarResultados se le llama con el
argumento suma que es un dato Real. Este procedimiento se puede escribir
ası́:
Procedure SacarResultados(resultado : Real);
Begin { SacarResultados }
Writeln(’’);
Writeln(’La suma de la series es: ’, resultado:10:6);
End; { SacarResultados }
En el argumento se especifica el nombre que se va a utilizar en el procedimiento y también de qué tipo de dato se trata. El nombre no ha de coincidir
con el utilizado en el bloque original, puesto que la identificación de datos la
realiza el compilador por el orden en el que aparecen en el argumento y no
por el nombre.
90
8.3
CAPÍTULO 8. MODULARIDAD
Funciones
Muy parecida a la estructura Procedure es la estructura Function y se
utiliza para aquellos casos en los que se desea que el dato que se calcula en el
módulo independiente pueda aparecer en cualquier instrucción PASCAL en
el mismo lugar en el que aparecerı́a el dato. Este es el caso de las funciones
intrı́nsecas del PASCAL que ya se han visto. La sintaxis de la estructura
Function es:
Function nombre ( argumentos ) : Tipodato ;
Parte declarativa
Begin
Bloque de sentencias de la función
End;
y en el bloque se especifica el valor que debe devolver la función asignándoselo
al nombre utilizado para nombrar la función. Por ejemplo, en el procedimiento CalcularExpresion debemos incluir el cálculo de la potencia de un
numero real a un exponente entero. Esta operación, que puede aparecer en
muchos otros programas, merece la pena que se escriba en un módulo aparte,
y dado que el resultado es un dato que suele aparecer en expresiones aritméticas, es razonable que se escriba como Function. A la función le llamamos
Poten:
{Funcion que eleva un numero real a un potencia entera }
Function Poten ( base : Real;
exponente : Integer ) : Real;
Var
i : Integer;
z : Real;
Begin { Poten }
{1}
If exponente <= 0 Then
{2}
Poten :=1
{3}
Else
{4}
If exponente = 1 Then
{5}
Poten := base
{6}
Else
{7}
Begin
{8}
z := base;
8.4. ÁMBITO DE DEFINICIÓN DE LAS VARIABLES
91
{9}
For i := 2 to exponente Do
{10}
z := z * base;
{11}
Poten := z
{12}
End;
End; { Poten }
En el argumento se especifican las dos variables con las que nos vamos a
referir al exponente y a la base, el exponente ha de ser un valor entero y
la base puede ser real. A continuación, se especifica que el valor que ha de
devolver la función al bloque donde ha sido llamada es Real. Para realizar
el cálculo se necesitan otras dos variables y se definen en la parte declarativa.
En las instrucciones {2}, {5} y {11}, aparece el nombre de la función en
la parte izquierda de una asignación y es en ellas donde se especifica el valor
que puede devolver la función. La llamada a Poten desde el procedimiento
CalcularExpresión :
{Calculo de la expresion para la suma de la serie }
Procedure CalcularExpresion ;
Begin { CalcularExpresion }
suma := a * ( 1.0 - Poten(r,n) ) / ( 1.0 - r) ;
End; { CalcularExpresion }
se realiza dentro de una expresión aritmética.
8.4
Ámbito de definición de las variables
Las variables que se definen en la parte declarativa de un procedimiento
o una función existen solamente para ese módulo. Se reservan lugares de
memoria para acomodarlas cuando se requiere el cálculo del módulo y se
liberan cuando se termina la ejecución de los algoritmos especificados en
el módulo. Por tanto, si en algún otro módulo del programa, o en la parte
principal, se utilizan esas variables, el compilador avisará de que no se conoce
el tipo de dato de esos identificadores. Por otro lado, se pueden utilizar
los mismos nombres para variables definidas en distintos módulos ya que el
compilador interpretará correctamente el hecho de que con cada nombre se
refiere al lugar de memoria reservado para el dato que se va a manipular en
cada módulo. El único modo que hay para pasar valores de datos desde un
92
CAPÍTULO 8. MODULARIDAD
módulo a otro, es a través de los argumentos del Procedure o Function.
Sin embargo, desde el bloque principal se pueden pasar datos a los procedimientos o funciones : cualquier variable o constante definido en la parte
declarativa del bloque principal es accesible a todos los módulos declarados
para el programa (tanto para conocer su valor como para modificarlo). Por
eso, el Procedure CalcularExpresion sin tener ningún argumento puede
acceder al valor de los datos almacenados en las variables n,a y r. Con
esta posibilidad parece lógico preguntarse el porqué de utilizar argumentos
en los módulos. En particular, el porqué de los argumentos en la función
Poten. Pues bien, la ventaja de utilizar los argumentos base y exponente
es que, aparte de mejorar la legibilidad, el mismo código puede utilizarse
en otros programas independientemente de cómo se llamen las variables que
intervienen en el cálculo.
8.5
Paso de valores por contenido o dirección
Lo dicho anteriormente sobre las variables definidas en las partes declarativas
de los módulos, también es cierto para los argumentos: se reservan para
esos datos nuevos lugares de memoria cada vez que se llama a los módulos,
se copia a ellos los datos almacenados en los argumentos, se utilizan en el
procedimiento y luego se liberan. Por tanto, dentro de un módulo no se puede
alterar el valor de las variables definidas para el bloque que llama al módulo.
Cuando se desee modificar en el módulo el valor de una variable pasada
como argumento hay que especificarlo y ası́ será accesible al módulo no sólo
el contenido de la variable sino también su dirección en la memoria. La
especificación se realiza anteponiendo a estas variables la palabra reservada
Var. Por ejemplo, en el procedimiento Leerdatos
Procedure Leerdatos( Var numero: Integer;
Var primero , razon : Real );
Begin {Leerdatos}
Writeln (’ Primer termino de la serie? ’);
Readln (primero);
Writeln (’ numero de terminos? ’);
Readln (numero);
Writeln (’ Razon de la serie? ’);
Readln (razon);
End; {Leerdatos}
8.5. PASO DE VALORES POR CONTENIDO O DIRECCIÓN
93
se especifica que las variables numero, primero, y razon, son argumentos
pasados al módulo por referencia o dirección y no por contenido o valor.
Los datos que almacena el procedimiento en numero, primero, y razon, en
realidad se están almacenando en los lugares de memoria que el programa
principal reservó para n, a, y r.
El programa completo que realiza la suma de la progresión es:
Program SumaProgresion;
Var
n : Integer;
r,a,suma : Real;
{ Lectura de datos }
Procedure Leerdatos( Var numero: Integer;
Var primero , razon : Real );
Begin {Leerdatos}
Writeln (’ Primer termino de la serie? ’);
Readln (primero);
Writeln (’ numero de terminos? ’);
Readln (numero);
Writeln (’ Razon de la serie? ’);
Readln (razon);
End; {Leerdatos}
{Funcion que eleva un numero real a un potencia entera }
Function Poten ( base : Real;
exponente : Integer ) : Real;
Var
i : Integer;
z : Real;
Begin { Poten }
If exponente <= 0 Then
Poten :=1
Else
If exponente = 1 Then
Poten := base
Else
Begin
94
CAPÍTULO 8. MODULARIDAD
z := base;
For i := 2 to exponente Do
z := z * base;
Poten := z
End;
End; { Poten }
{Calculo de la expresion para la suma de la serie }
Procedure CalcularExpresion ;
Begin { CalcularExpresion }
suma := a * ( 1.0 - Poten(r,n) ) / ( 1.0 - r) ;
End; { CalcularExpresion }
Procedure SacarResultados(resultado : Real);
Begin { SacarResultados }
Writeln(’’);
Writeln(’La suma de la series es: ’, resultado:10:6);
End; { SacarResultados }
Begin { SumaProgesion }
{ Se utiliza la expresion:
suma = a (1-r**n) / (1-r) }
LeerDatos(n,a,r);
CalcularExpresion;
SacarResultados(suma);
End. { SumaProgresion}
El ámbito de las variables y la diferencia entre el paso de argumentos por
referencia o contenido se puede apreciar bien con el siguiente programa Uno:
Program Uno;
Var
a , b ,c : Integer;
8.5. PASO DE VALORES POR CONTENIDO O DIRECCIÓN
95
Procedure Escribe ( texto : String;
Var x : Integer;
y : Integer );
Begin { Escribe }
x := 2 * x ;
y := 2 * y ;
Writeln (texto, x:4 , y:4 , c:4 );
End; { Escribe }
{1}
{2}
{3}
{4}
{5}
Begin
a := 3;
b := 5;
Writeln(’ Antes :
Escribe(’Durante:
Escribe(’Durante:
Writeln(’Despues:
End.
’,
’,
’,
’,
c:=
a:4
a ,
a ,
a:4
7;
, b:4 , c:4 );
b );
b );
, b:4 , c:4 );
que produce la siguiente salida:
Antes :
Durante:
Durante:
Despues:
3
6
12
12
5
10
10
5
7
7
7
7
La variable c la conoce el procedimiento Escribe porque está declarada
como variable global para todo el programa. La variable y se pasa por
contenido y por tanto su multiplicación por 2 en el procedimiento no altera el
valor del dato almacenado en la variable global b. Sin embargo, el argumento
x se pasa por referencia y por tanto cada vez que se llama al procedimiento
se multiplica por 2 el valor almacenado en la variable global a. Cuando
una variable se pasa por referencia el compilador ha de interpretar que cada
vez que se asigne un nuevo valor a esa variable se está almacenando un
nuevo valor en el contenido de la dirección de memoria que corresponde a
esa variable. Por eso se dice que se está transmitiendo al procedimiento la
dirección de la variable.
Comprendiendo lo anterior está claro que las constantes no pueden ser
trasmitidas como argumentos pasados por referencia: las constantes estás en
posiciones en las que no se puede escribir durante la ejecución del programa.
Por ejemplo, si el procedimiento Escribe se hubiera definido
96
CAPÍTULO 8. MODULARIDAD
Procedure Escribe ( Var texto
Var x
y
: String;
: Integer;
: Integer );
el compilador avisarı́a de que en la instrucción {3} se espera como primer
argumento del procedimiento una variable.
8.6
Definición diferida
El compilador transforma las ordenes PASCAL a lenguaje máquina empezando por el principio del programa y acabando por el End. Por tanto,
cualquier procedimiento o función ha de declarase antes de ser utilizado para
que el compilador cuando llegue al identificador correspondiente sepa interpretar qué instrucciones de la Unidad Central de Proceso debe generar. En
general esto implica un orden mı́nimo a la hora de escribir el programa y llevar cuidado de no definir módulos en lı́neas posteriores a las que los utilizan.
Sin embargo, puede haber ocasiones en las que se requiera que un módulo
llame a otro módulo y que a su vez llame al primero. En este caso serı́a imposible definir los dos antes de referirlos y por tanto PASCAL proporciona la
palabra reservada FORWARD para seguir al nombre y argumentos de un
procedimiento o función cuyo contenido se especificará en lineas posteriores.
En el programa Dos se ilustra el uso de FORWARD :
Program Dos;
Procedure B ( C : Char); FORWARD;
Procedure A ( c : Char);
Begin { A }
If c < ’Z’ Then B (c);
Write ( c )
End; { A }
Procedure B ( c : Char);
Begin { B }
A ( Succ (c) )
End; { B }
8.7. MÓDULOS Y SUBMÓDULOS
97
Begin { Dos }
A ( ’A’ );
End. { Dos }
El procedimiento B es necesario para definir el contenido de A, y a su vez
A es necesario para definir el algoritmo siniestro que se explicita en B. El
circulo vicioso se rompe adelantando al compilador que B es un Procedure
que toma como argumento un dato Char. Cuando el compilador luego
analiza el procedimiento A reconoce el identificador B y el hecho de que
tome un argumento Char. Entonces, podrá generar código de la UCP en el
que redireccione el cálculo a la dirección de memoria que ya tiene reservada
para el módulo B. Posteriormente, cuando analice la especificación de los
pasos a realizar en el módulo B generará el código UCP que constituirá
el cálculo que se ha de realizar en el procedimiento B. Si todo esto parece
enrevesado más lo es el modo en que se ha programado el algoritmo que
realiza el programa Dos. Además, carece de ningún comentario para que
se pueda calificar como un claro ejemplo de mala programación. El alumno,
realizando las instrucciones del programa a mano tardará algún minuto en
conocer cuál es la salida del programa.
8.7
Módulos y submódulos
Al igual que para un programa se pueden definir módulos, también para un
módulo se pueden escribir otros módulos. Es decir declaraciones de procedimientos cuyo significado sólo conoce el módulo en el que se definen. Por
ejemplo en el programa Tres la función Final es local al procedimiento Eco.
Program Tres;
Procedure Eco;
Var
s : String;
Function Final : Boolean;
Var
c : Char;
Begin { Final }
98
CAPÍTULO 8. MODULARIDAD
Writeln(’ Acabo de hacer el ecooooo ? (s/n) ’);
Readln(c);
If
(c = ’s’) Or (c = ’S’)
Then
Final := True
Else
Final := False
End; { Final }
Begin { Eco }
Repeat
Readln(s);
Writeln(s);
Until Final;
End; { Eco }
Begin { Tres }
{1}
{2}
Eco;
End. { Tres }
de modo que si en la linea etiquetada 1 del cuerpo del programa principal se
añade la instrucción:
If Final Exit;
para que el usuario pueda acabar tras la primera pregunta, el compilador
avisará de que el identificador Final no está definido. Esta función está
definida localmente para el procedimiento Eco exactamente como también
es local a él la variable s. Esta posibilidad de anidamiento en la definición
de módulos no se debe utilizar salvo en raras ocasiones pues es origen de
muchas confusiones y es mejor optar por un estilo homogéneo en el que sólo
se definen módulos para el programa principal
Un error muy común en la definición del contenido de las funciones es el
olvidar asignar un valor al identificador de la función. Generalmente no se
trata de una negligencia sintáctica sino del olvido de una posibilidad cuando
se hacen asignaciones condicionales. Pero los efectos de este error son menos
alarmantes que los de utilizar el identificador de la función en la parte derecha
de una asignación en el cuerpo de la función. En ese caso, lo más probable es
8.8. RECURSIVIDAD
99
que se haya escrito un programa que realice un bucle infinito. Esta posibilidad
existe puesto que el PASCAL permite la recursividad en la programación:
algo que bien utilizado puede aumentar la legibilidad de los programas.
8.8
Recursividad
La definición de muchos objetos abstractos se realiza mediante recursión. La
recursión aparece generalmente o bien en la definición de estructuras con
autosemejanza o en la descripción de procedimientos de cálculo matemático.
Se dice que una definición es recursiva cuando el objeto definido aparece
en la recursión. Este es un modo de definir objetos muy común y útil. Por
ejemplo, cuando estudiamos el problema de Josefo, se definı́an los nodos
que componı́an el cı́rculo como componentes que se identificaban por un
número y tenı́an un enlace con otro nodo. Como veremos posteriormente, la
recursividad es muy útil para definir estructuras de datos.
La recursión también se utiliza para definir procedimientos de cálculo.
En este caso, hay que llevar cuidado en transformar la definición recursiva
en un algoritmo que se realice en un número finito de pasos. Por ejemplo,
consideremos la definición del factorial de un número que se deriva de la
siguiente expresión:
n! = n(n − 1)!
Esta igualdad matemática no es un procedimiento de cálculo. Para que ası́
lo sea, hay que añadir algo más. En este caso, se trata de algo tan sencillo
como una condición de principio o fin. Si consideramos las dos igualdades:
n! = n(n − 1)!
1! = 1
veremos que ya disponemos de un procedimiento para calcular el factorial.
Para calcular, por ejemplo, 4! utilizarı́amos la secuencia de igualdades:
4! = 4 × 3! = 4 × 3 × 2! = 4 × 3 × 2 × 1! = 4 × 3 × 2 × 1 = 24
La definición recursiva, y una receta para conocer un valor particular, nos ha
proporcionado un método muy claro para calcular el factorial de cualquier
número entero n ≥ 1. En general una definición recursiva de un algoritmo
incluye:
100
CAPÍTULO 8. MODULARIDAD
• La relación con el caso mas pequeño.
• La condición de finalización.
El PASCAL , como cualquier lenguaje moderno de programación permite la programación recursiva: en la definición de un módulo, Procedure
o Function, puede aparecer una llamada a él mismo. El programa para
calcular el factorial podrı́a ser:
Program Factoriales;
Var
m :Integer;
Function Fac ( n : Integer ) : Integer;
{ Utiliza la definicion n! = n (n-1)! }
Begin
{1} If n <= 1 Then
{2}
Fac := 1
{3}
Else
{4}
Fac := n
End;
* Fac ( n - 1 )
Begin
Writeln(’Calculo de factorial. Introducir el numero:’);
Read( m );
If m > 7 Then
Writeln(’El numero es demasiado grande.’)
Else
Writeln (’El factorial de’, m:2 , ’ es ’, Fac (m) )
End.
Como se utilizan datos Integer este programa sólo será capaz de calcular
hasta el factorial de números pequeños pues crece muy rápidamente y a partir
de 7! se desborda la capacidad de almacenamiento de ese dato. Por lo demás,
se trata de un programa de fácil lectura. En la instrucción {2} consideramos
el valor de terminación de las llamadas recursivas. Sin ella, el programa no
acabarı́a nunca. En la instrucción {4} estamos diciendo que en esa llamada
se asigne a la función Fac el valor que se obtiene multiplicando el valor actual
8.8. RECURSIVIDAD
101
de n por el resultado que se obtenga al llamar a la función Fac con el valor
decrementado en 1.
Generalmente, la recursividad mejora la facilidad de escritura de algoritmos y su legibilidad. Sin embargo, los programas que resultan suelen ser
menos eficientes que aquellos que hacen la misma tarea sin usar la recursividad (en el programa Catorce del tema anterior vimos el caso no recursivo). Muy frecuentemente, se utiliza el algoritmo recursivo como una primera
aproximación a la resolución del problema. Posteriormente, un análisis más
profundo de éste, puede llevar a otro algoritmo no recursivo y que se ejecuta
con menos gasto de recursos. La primera versión recursiva, conceptualmente
más sencilla, siempre sirve para comparar los resultados del algoritmo más
elaborado.
Como se mencionó anteriormente, la posibilidad de recursividad que ofrece el PASCAL puede dar lugar a errores de programación con resultados molestos. Si por error en una función aparece el nombre de una función en el
lado derecho de una asignación, el compilador no avisará de tal error, pues
sintácticamente corresponde a una llamada recursiva, y es muy probable que
se obtenga un bucle infinito.
102
CAPÍTULO 8. MODULARIDAD
Capı́tulo 9
Datos con estructura
Un programa de ordenador está constituido por algoritmos que manejan la
información almacenada en la memoria del ordenador. El modo en el que
esta información puede ser referenciada por el programador es esencial para
la eficacia y legibilidad de los algoritmos. Hasta ahora hemos visto tipos de
datos muy elementales. En este tema se consideran opciones más avanzadas
al estudiar los tipos de datos definidos por el programador y datos que poseen
estructura interna.
9.1
Tipos de datos definidos por el programador
Ya se han estudiado tipos de datos elementales como los Integer, Real,
Char, etc. En este tema se estudiarán otros tipos de datos definidos en
PASCAL . Pero también puede el programador definir tipos de datos a la
medida de sus necesidades. Con la palabra reservada Type se puede informar
al compilador sobre la interpretación de variables que van almacenar el nuevo
tipo de dato. Esta definición de nuevos tipos de datos se incluye en la parte
declarativa del programa, antes de que se referencie ese nuevo tipo de dato,
y con la siguiente sintaxis:
Type
Indentificador1 = Untipo1;
Indentificador2 = Untipo2;
.
.
.
103
104
CAPÍTULO 9. DATOS CON ESTRUCTURA
donde Indentificador1,Indentificador2, etc., son los nombres que se han asignado a los nuevos tipos de datos , y Untipo1,Untipo2, etc., es la especificación
de qué tipo de dato se trata.
La utilización más trivial que se puede pensar para esta posibilidad es la
de renombrar la denominación de los tipos de datos estandard. Por ejemplo,
Program Uno;
Type
Entero = Integer;
Var
i,n : Entero;
. . . . . .
con estas declaraciones se informa al compilador de que cuando se definen las
variables i,n como Enteros , nos referimos a datos Integer. Obviamente,
la posibilidad de definición de tipos de datos existe para realizar cosas más
útiles que simplemente renombrar. Por ejemplo, se puede utilizar para definir
variables enteras con un rango acotado. En el programa Uno se utiliza esta
posibilidad:
Program Uno;
Type
Nota = 0..10;
Var
i : Nota;
Begin
Writeln(’Teclear la nota (0 a 10):’);
Readln(i);
Case i Of
0..4 : Writeln(’Suspenso’);
5..10 : Writeln(’Aprobado’)
End;
End.
Se define el tipo de dato Nota que es un entero en el rango entre 0
y 10. La primera de las instrucciones del programa es una orden para el
compilador de TURBO PASCAL , que le indica que genere código de UCP
en el que se realice comprobación de rangos de variables durante la ejecución
9.2. ENUMERACIONES
105
del programa. Con esta opción, si se teclea un dato fuera del rango definido
para Nota aparecer? un mensaje de error Run time. Sin esa opción del
compilador, no aparecerı́a ningún mensaje de error.
9.2
Enumeraciones
Los tipos de datos Integer, Char y Byte son ejemplos de tipos de datos
en los que se puede almacenar un número limitado de datos. En el tipo
Byte, por ejemplo, números entre el 0 y el 255. Si el programador necesita
utilizar un conjunto limitado de datos puede referirse a ellos estableciendo
explı́citamente una relación entre cada dato y uno de los valores que puede tomar un tipo de dato limitado. Por ejemplo, si queremos referirnos a
los meses de año en un programa se puede establecer una relación entre un
número y el mes, empezando con el 1 para Enero y siguiendo hasta 12 con
el orden del calendario. Dado el uso cotidiano de esta relación resultarı́a
muy fácil escribir y leer programas en los que se utilizara una variable mes
del tipo Byte. Siempre que mes tomara el valor 2 el programador inmediatamente interpretarı́a ese valor como el mes de Febrero. Sin embargo, en
otras muchas ocasiones no existirá tal correlación cotidiana entre ordinales
y objetos. Por ejemplo, si en un programa se quiere operar con datos del
tipo color, considerando las posibilidades Rojo , Amarillo , Naranja, Verde
, y Azul, no es fácil elegir una ordenación entre ellos que permita establecer
una relación fácilmente comprensible entre un número, por ejemplo del 0 al
4, y cada color. Para estos casos el PASCAL permite establecer esa relación
en la definición de un tipo y el programador puede olvidarse de ella durante
el programa. Un tipo de dato enumerado se especifica con el conjunto de
posibilidades entre paréntesis y separadas por comas. En el programa Dos
tenemos un ejemplo de su utilización:
Program Dos;
Type
Color = (Rojo , Amarillo , Naranja, Verde , Azul);
Var
i : Color;
Begin
i := Amarillo;
Case Succ(i) Of
Amarillo :
Writeln(’Se trata de Amarillo’);
106
CAPÍTULO 9. DATOS CON ESTRUCTURA
Naranja
End;
:
Writeln(’Se trata de Naranja’);
Writeln(Ord(i));
End.
El tipo de dato Color se define como la enumeración de esos cinco colores.
La variable i se define como un Color y por tanto puede tomar el valor
Amarillo. El compilador establece una relación unı́voca entre cada uno de
los posibles valores y un número. Por tanto sobre este tipo de dato se puede
operar con las funciones que admiten argumentos ordinales, entre otras Succ
y Pred. Este programa escribirá primero : Se trata de Naranja porque el
sucesor del valor Amarillo es Naranja. De hecho, la relación que establece
el compilador entre valores y números es sencillamente el número de orden,
empezando por 0, en el que aparece cada valor en la enumeración que se
utiliza para declarar el tipo. Por tanto, el programa acabará su ejecución
escribiendo el número 1.
Este tipo de datos enumerado se suele utilizar para facilitar la tarea de
programar (escritura y lectura), y no se pueden leer o escribir. Por ejemplo,
la instrucción Readln(i); no serı́a válida en el programa Dos. Sin embargo,
se puede establecer la relación entre un ordinal y el valor de un tipo de dato
enumerado que aparece en ese lugar en la definición. Por ejemplo, la primera
instrucción del programa Dos se podrı́a reemplazar por
i:= Color(1);
Otra utilidad de los tipos de datos enumerados es la formación de conjuntos con la posibilidad que ofrece el PASCAL de operaciones entre conjuntos.
9.3
Conjuntos
Un conjunto de datos del tipo Cualquiera se define con la siguiente sintaxis:
Type identificador Set Of Cualquiera
donde identificador es el nombre elegido para el nuevo tipo de dato, y cualquiera es el tipo de dato de los elementos que forman el conjunto. Los
elementos pueden ser cualquier tipo de dato ordenado, y entre ellos los enumerados.
9.3. CONJUNTOS
107
La especificación de los miembros de un determinado conjunto se realiza
escribiendo los elementos, uno a uno, o mediante rangos, entre paréntesis
cuadrados. El el programa Tres se ilustra el uso de los conjuntos:
Program Tres;
Type
Meses = ( enero , febrero , marzo , abril , mayo , junio,
julio , agosto , septiembre , octubre,
noviembre, diciembre );
Estacion = Set Of Meses;
Var
n : Integer;
mes : Meses;
otogno,invierno,primavera,verano,cambio: Estacion;
Begin
invierno := [diciembre ,enero .. marzo ];
primavera := [marzo .. junio];
verano :=
[junio .. septiembre];
otogno :=
[septiembre .. diciembre];
{ Los meses de cambio de estacion son la suma de las
interseciones de las estaciones }
cambio := invierno * primavera + primavera * verano +
verano * otogno + otogno * invierno;
Writeln(’teclear en numero del mes (de 1 a 12):’);
Readln(n);
mes := Meses( n - 1);
If mes In cambio
Then Write(’ Cambia la estacion :
If mes In verano
Then Write(’ verano ’
If mes In otogno
Then Write(’ otogno ’
If mes In invierno Then Write(’ invierno ’
If mes In primavera Then Write(’ primavera ’
Writeln(’’);
’);
);
);
);
);
End.
El operador In se utiliza para determinar si un elemento se encuentra dentro
de un conjunto, con la siguiente sintaxis:
108
CAPÍTULO 9. DATOS CON ESTRUCTURA
NomElemento In NomConjunto
donde NomElemento es el nombre del elemento sobre el que se inquiere y
NomConjunto el conjunto. El resultado de esta operación es un dato Boolean, y se evalúa a True sólo si el elemento pertenece al conjunto.
En el programa Tres también se ilustra la asignación de valores a los
conjuntos, si bien falta añadir que también se puede asignar el conjunto
vacı́o ([] ).
Los operadores aritméticos y lógicos cuando actúan entre conjuntos tienen
un significado nuevo. Si A, B y C son conjuntos de elementos del mismo tipo,
las operaciones permitidas son :
C := A + B
C := A * B
C := A - B
{C : conjunto
{C : conjunto
{C : conjunto
union de A y B}
interseccion de A y B}
diferencia de A y B}
También se pueden comparar los conjuntos con los operadores lógicos dando
lugar a un dato Boolean. Si A y B son dos conjuntos de elementos del
mismo tipo y verdad un dato Boolean, las comparaciones siguientes son
posibles:
verdad
verdad
verdad
verdad
:=
:=
:=
:=
A
A
A
A
=
B
<> B
<= B
=> B
{verdad es True si A y
{verdad es True si A y
{verdad es True si A es
{verdad es True si B es
B son iguales}
B son distintos}
subconjunto de B}
subconjunto de A}
teniendo en cuenta que el conjunto vacı́o es subconjunto de todo conjunto.
También hay que tener en cuenta que el número de elementos que pueden
formar un conjunto en PASCAL está limitado a un máximo de 256.
9.4
Arrays
Cuando se tiene que seguir la pista a un grupo o de datos es muy útil referirse a todos ellos con un mismo nombre y distinguir entre los elementos
mediante el lugar que ocupan en el grupo. En realidad de trata de ampliar la
conveniencia de los conjuntos estudiados anteriormente a grupos de datos de
cualquier tipo. En el Array los elementos del grupo se ordenan asignándose
a cada elemento un número de orden o dirección. El tipo de dato Array (
tabla ) es uno de los datos con estructura más importantes de un lenguaje.
Esto es ası́, porque corresponde a una ordenación de datos similar a la que se
9.4. ARRAYS
109
realiza en la memoria central del ordenador. Se puede pensar que un Array
es un conjunto de celdillas contiguas, en cada una de ellas se puede almacenar un dato simple, y cada dato simple está identificado por la posición que
ocupa en esa tabla. La ventaja de esta estructura es que se tarda el mismo
tiempo en acceder a cualquier elemento, ya que se accede a él exclusivamente
por su dirección. La desventaja, es que se trata de un estructura estática en
el sentido de que la longitud máxima de la tabla se ha de especificar a priori.
Para definir el tipo de dato Array se utiliza la siguiente sintaxis:
Array [ rango ] Of tipo
donde tipo es el tipo de datos que se almacena en cada una de las posiciones
de la tabla y rango la especificación del rango de variación del ı́ndice (entero)
que identifica las posiciones de los elementos de la tabla. Este rango se
concreta especificando el primer y último ı́ndice separados por dos puntos
consecutivos. Sólo se permite un rango consecutivo.
La especificación de Array puede aparecer tanto en una definición de
tipo de dato, por ejemplo,
Type
vector = Array [1..30] Of Real;
o directamente en la declaración de una variable, por ejemplo,
Var
texto : Array [0..3000] Of Char;
El ı́ndice que sirve para especificar la posición de los elementos no tiene
porque empezar en 0 o 1. Si para el programa tiene sentido utilizar un rango
como [127..345], es perfectamente válido. Para acceder a un elemento de
un Array se utiliza el identificador seguido de su ı́ndice entre paréntesis
cuadrados.
En el programa Primos tenemos un ejemplo de utilización de esta estructura de datos. Se trata de la determinación de números primos utilizando el
famoso algoritmo de la criba de Eratóstenes.
Program Primos;
Const
MAX = 1000;
{ Calculo de los numeros primos entre 1 y MAX
utilizando el algoritmo de la Criba de Eratostenes }
110
CAPÍTULO 9. DATOS CON ESTRUCTURA
Var
esPrimo : Array [1..MAX] of Boolean;
i,j : Integer;
Begin { primos }
{Se inicializa la tabla}
esPrimo[1] := False;
For i := 2 to MAX Do esPrimo[i] := True;
{En la criba de Eratostenes se parte de todos los numeros
y se van eliminando todos los multiplos de los primos
elegidos. Seran primos elegidos aquellos que , en orden
ascendente, no hayan sido marcados como multiplos }
For i := 2 To MAX Div 2 Do
For j := 2 To MAX Div i Do
esPrimo[i*j] := False;
{Se cuenta el numero de primos encontrados }
j:= 0; For i := 1 To MAX Do If esPrimo[i] Then j := j + 1;
Writeln(’Entre 1 y ’,MAX:5,’ se han encontrado ’, j:3,’ primos’);
For i := 1 To MAX Do If esPrimo[i] Then Write(i:4);
End. { primos }
Se define el Array esPrimo para almacenar datos Boolean que permiten
seguir la pista de los números que son múltiplos de otros. En este programa
se hace un uso apropiado de la ventaja ofrecida por la estructura de dato
Array. Se utiliza un tiempo constante para acceder a cualquier elemento de
la tabla, independientemente de que se trate del primero o el último. Además
se puede incluir en el programa una relación ventajosa entre la posición de
un elemento en la tabla y su significado.
La estructura Array se usa muy a menudo en el cálculo cientı́fico, pues
se trata de la realización de un vector, si cada elemento de la tabla se interpreta como un componente del vector. Igualmente, se pueden definir tablas
de tablas (Array bidimensional) para manipular matrices, y Array multidimensionales para los tensores.
La especificación del rango, cuando se declaran, en los Array multidimensionales se puede realizar separando con comas los rangos de cada una
de las dimensiones. Igualmente, el acceso al contenido de una posición de
9.4. ARRAYS
111
estas tablas múltiples se puede realizar separando con comas los ı́ndices de
cada una de las dimensiones.
El siguiente procedimiento MultMatriz se puede utilizar para multiplicar matrices.
Program Cuatro;
Const
MAXDIM = 20;
Type
Numeros = Real;
Matriz = Array[1..MAXDIM,1..MAXDIM] Of Numeros;
Var
n : Integer;
A,B,C : Matriz;
Procedure Leematriz ( Var X : Matriz); FORWARD;
Procedure Escribematriz ( X : Matriz); FORWARD;
Procedure MultMatriz ( dim : Integer;
Var
A1 , A2 , M : Matriz );
Var
i,j,k : Integer;
x : Numeros;
Begin { MultMatriz }
For i := 1 To dim Do
For j := 1 To dim Do
Begin
x:= 0.0;
For k := 1 To dim Do
x := x + A1 [i,k] * A2 [k,j];
M[i,j] := x;
End; {End del doble For}
End; { MultMatriz }
{ Aqui se encontrarian las definiciones de los
}
{
dos procedimientos que se han omitido
}
Begin
Writeln(’Dimension de las matrices: ’);
Readln(n);
Leematriz(A); Leematriz(B);
112
CAPÍTULO 9. DATOS CON ESTRUCTURA
MultMatriz(n,A,B,C);
Writeln(’La Matriz producto es:’);
Escribematriz(C);
End.
Pero la definición de los Array miltidimensionales también se puede hacer
como una tabla de tablas. Por ejemplo, el tipo de dato Matriz se podrı́a
haber definido del siguiente modo:
Matriz = Array[1..MAXDIM] Of Array [1..MAXDIM] Of Numeros;
e igualmente los componentes de este tipo de datos se pueden referir como:
x := x + A1 [i][k] *
A2 [k][j];
Ambas definiciones y utilizaciones son equivalentes. Las limitaciones de las
estructuras Array definidas de este modo están en la capacidad de almacenamiento. Una estructura Array no puede ocupar toda la memoria disponible
en el ordenador por muchos motivos que se estudiaran en el tema de gestión
de memoria. Basta con uno de ellos: en TURBO PASCAL no se puede crear
un Array que ocupe más de un segmento de memoria (216 = 65, 536 bytes ).
La estructura de datos String es simplemente un caso especial de tabla de
datos tipo Char en el que se reserva la posición de ı́ndice cero para almacenar
el carácter correspondiente al número que indica los elementos almacenados.
Ya vimos en su dı́a que el tipo de datos String tiene su capacidad limitada a
255 caracteres. El programador puede muy fácilmente definir un tipo de dato
similar al String con capacidad superior. En este caso, tendrá que definir
sus propias funciones para manipularlos. La siguiente función Concatena
podrı́a ser la función que uniera dos de estas cadenas de caracteres.
Program Cinco;
Const
MAXDIM = 300;
{ Los caracteres se guardan en enteros como su ordinal }
Type
Cadena = Array[0..MAXDIM] Of Integer;
Var
A,B : Cadena;
Procedure LeeCadena (Var x : Cadena);
9.4. ARRAYS
113
Var
i : Integer;
c : Char;
Begin { LeeCadena }
i:= 1;
Repeat
Read(c);
x[i] := Ord(c);
i := i+1;
Until ( EOLN );
x[0] := i - 1;
{ Lee los caracteres ASCII 13 (CR) y 10 (LF)
que delimitan el fin de linea
}
Read(c);Read(c);
End; { LeeCadena }
Procedure EscribeCadena ( x : Cadena);
Var
i: Integer;
Begin { EscribeCadena }
For i := 1 to x[0] Do Write(Chr( x[i] ));
End; { EscribeCadena }
{ Procedimiento para unir a la cadena A1 la cadena A2 }
Procedure Concatena ( Var
A1 , A2 : Cadena );
Var
dim1 , i : Integer;
Begin
dim1 := A1[0];
For i := 1 To A2[0] Do
A1[ dim1 + i ]
A1[0] := dim1 + A2[0];
End;
Begin
Writeln(’’);
:= A2[i];
114
CAPÍTULO 9. DATOS CON ESTRUCTURA
LeeCadena(A);
LeeCadena(B);
Concatena(A,B);
Writeln(’’);
EscribeCadena(A);
End.
Se aprecia en la función Concatena que el hecho de poder acceder a la
longitud de la cadena permite escribir algoritmos muy eficientes de cadenas
de caracteres. Sin embargo, esta estructura definida para las cadenas de
caracteres es ineficiente en cuanto almacenamiento. Se están almacenando
los caracteres ASCII en 2 bytes mientras que serı́a suficiente uno sólo.
9.5
Registros
La situación encontrada en el problema planteado anteriormente de manipulación de cadenas es muy común. Lo más frecuente es encontrarse en una
situación en la que se quiere representar en un tipo de dato una información
que posee estructura interna y es heterogénea, es decir, los campos en los que
se subdivide el dato no son todos del mismo tipo. Para ello el PASCAL, como
otros lenguajes de programación actuales, suministra los datos tipo registro
(en inglés records).
Un registro es un tipo de dato definido por el programador en el que
puede especificar su estructura interna.
El programador da nombre al nuevo tipo de dato y a cada uno de los
campos que lo componen, y especifica el tipo de dato que puede ocupar cada
uno de los campos. La sintaxis para estas especificaciones es:
Type
nombre =
Record
NombreCampo1 : TipoDato1 ;
NombreCampo2 : TipoDato2 ;
.
.
.
End;
donde nombre es el identificador elegido para el registro, y NombreCampo1 , NombreCampo2 , ... son los identificadores elegidos para los distintos
9.5. REGISTROS
115
campos. TipoDato1 , TipoDato2, ... son la especificación del tipo de dato
que se va a almacenar en cada uno de los campos. Esta especificación puede
corresponder a un tipo de dato estandard del PASCAL , la enumeración o el
rango de los posibles valores, u otro tipo de dato declarado con anterioridad.
Por ejemplo, hay casos en los que puede ser conveniente definir un tipo de
variable para almacenar la información de una fecha. Una posible definición
es la siguiente:
Program Seis;
Type
Fecha =
Record
mes : 0 .. 12; { 0 seria para indicar que no se conoce la fecha}
dia : 1 .. 31;
agno : Integer ;
End;
Var
alta , baja : Fecha;
El modo en el que se especifica refiriéndose a una variable del tipo Record
un determinado campo, es añadiendo al nombre de la variable el nombre
dado al campo, y unidos por un punto. El ejemplo anterior podrı́a continuar
del siguiente modo
Begin
alta.dia := 27 ;
alta.mes := 2 ;
alta.agno := 1992;
baja.mes := alta.mes + 2;
....
También el PASCAL permite la comodidad de la estructura With para acceder a los valores de los campos de los registros. La sintaxis de la estructura
With es la siguiente :
With nombre Do accion
donde nombre es la especificación de la variable ( o las variables separadas por
comas ) del tipo Record a la que se refiere la instrucción PASCAL accion
116
CAPÍTULO 9. DATOS CON ESTRUCTURA
que se encuentran a continuación del Do. Si se trata de una instrucción
compuesta se encontrará, como siempre, horquillada entre un Begin y un
End. En la instrucción del With podemos especificar los distintos datos
del registro con sólo el nombre de los campos. Por ejemplo, las instrucciones
anteriores, se podrı́an haber escrito del siguiente modo:
Begin
With alta Do
Begin
dia := 27 ;
mes := 2 ;
agno := 1992;
baja.mes := mes + 2;
End;
....
La elección del uso de la estructura With se suele hacer en cada caso particular según la legibilidad que añada al programa.
Ya hemos dicho que los campos de los registros pueden contener datos que
son del tipo registro. En ese caso, como siempre, habrá que llevar cuidado en
definir los tipos de datos en el orden adecuado para que nunca aparezca en
una declaración un tipo de dato que no ha sido declarado en pasajes anteriores
del programa. Si ampliamos el ejemplo anterior hacia la construcción de una
base de datos con los alumnos y sus notas, un programa de captación de
datos podrı́a empezar ası́:
Program Siete;
Type
Fecha =
Record
mes : 0 .. 12; { 0 seria para indicar que no se conoce la fecha}
dia : 1 .. 31;
agno : Integer ;
End;
Alumno =
Record
nombre : String;
apellidos : String;
9.5. REGISTROS
117
nacimiento : Fecha;
nota : Real;
End;
Var
uno,otro : Alumno ;
Begin
Writeln(’Nombre : ’) ; Readln(uno.nombre);
Writeln(’Apellidos : ’); Readln(uno.apellidos);
Writeln(’Agnno de nacimiento : ’); Readln(uno.nacimiento.agno);
Writeln(’Mes de nacimiento: ’); Readln(uno.nacimiento.mes);
Writeln(’Dia de nacimiento: ’); Readln(uno.nacimiento.dia);
Writeln(’Nota del examen: ’); Readln(uno.nota);
. . . . . . . . . . . . . .
y vemos que la referencia a los campos de registros que son a su vez campos
de un registro se realiza concatenando con puntos los identificadores de los
campos.
Para crear una base de datos que contuviera información de los alumnos
de una clase lo lógico serı́a ordenar los alumnos en una lista. Para ello, se
puede crear una estructura Array cuyos elementos sean los registros definidos para almacenar la información de los alumnos. Se podrı́a por ejemplo
añadir en la parte declarativa del programa Siete el siguiente tipo de dato:
{
Type }
Lista =
Array[1..100] of alumno;
y también una variable de este tipo:
{ Var }
primero : Lista;
El acceso a los elementos del Array es el habitual. Por ejemplo, el programa
Siete podrı́a continuar del siguiente modo:
primero[1] := uno;
If (Primero[1].nacimiento.mes = 12 ) Then ......
118
CAPÍTULO 9. DATOS CON ESTRUCTURA
En la primera de estas instrucciones se asigna al elemento primero del Array
primero el dato almacenado en uno (con todos sus campos de una sola
vez). En la segunda, se comprueba el valor que tiene el dato almacenado
en el campo mes del campo nacimiento de la variable del tipo Alumno
almacenado en la posición primera del Array primero.
También existe, obviamente, la posibilidad inversa: la de definir registros
en los que sus campos sean datos con la estructura Array. Podemos retomar
ahora el problema estudiado anteriormente de cadenas de caracteres y utilizando registros disponer de una estructura de datos con aprovechamiento
eficiente de la memoria. El tipo de dato Cadena se define ahora como un
registro con un campo que es el número de caracteres almacenados y el otro
campo el Array de caracteres. Ası́, para la longitud utilizamos un Integer
y para cada carácter un sólo byte.
Program Ocho;
Const
MAXDIM = 300;
Type
Ristra = Array [1..MAXDIM] of Char;
Cadena =
Record
longitud : Integer;
contenido : Ristra;
End;
Var
A,B : Cadena;
Procedure LeeCadena (Var x : Cadena);
Var
i : Integer;
c : Char;
Begin { LeeCadena }
i:= 1;
With x
Do
Begin
Repeat
Read(c);
contenido[i] := c;
i := i+1;
9.5. REGISTROS
119
Until ( EOLN );
longitud := i - 1;
End;
{ Lee los caracteres ASCII 13 (CR) y 10 (LF)
que delimitan el fin de linea
}
Read(c);Read(c);
End; { LeeCadena }
Procedure EscribeCadena ( x : Cadena);
Var
i: Integer;
Begin { EscribeCadena }
For i := 1 to x.longitud Do Write(x.contenido[i] );
End; { EscribeCadena }
{ Procedimiento para unir a la cadena A1 la cadena A2 }
Procedure Concatena ( Var
A1 , A2 : Cadena );
Var
dim1 , i : Integer;
Begin
dim1 := A1.longitud;
For i := 1 To A2.longitud Do
A1.contenido[ dim1 + i ] := A2.contenido[i];
A1.longitud := dim1 + A2.longitud;
End;
Begin
Writeln(’’);
LeeCadena(A);
LeeCadena(B);
Concatena(A,B);
Writeln(’’);
EscribeCadena(A);
End.
Después de estudiar todos estos tipos de datos definidos por el usuario
120
CAPÍTULO 9. DATOS CON ESTRUCTURA
debe estar mucho más claro el sentido de los conceptos tipo de dato y variable. Una variable, es un identificador que utiliza el programador para
referirse a un dato y poder realizar operaciones con él. El tipo de dato ha de
especificarse para que el compilador pueda generar código UCP en el que se
utilice una cantidad de memoria suficiente para ese dato y estructurada del
modo adecuado. Cuando el programador se enfrenta a la resolución de un
problema puede pensar en variables para cualquier concepto abstracto que
piense que sea útil para resolver el problema de un modo claro y comunicable, lo que muchas veces quiere decir de un modo lo más próximo posible al
lenguaje natural. La restricción obvia es poder explicitar sin ambigüedades
la gestión de la memoria del ordenador que ha de realizar el compilador para
almacenar y manipular ese dato. Mediante la estructura Record y Array
el PASCAL ofrece la posibilidad de definir tipos de datos muy próximos a
los utilizados en el lenguaje natural y que son una organización precisa de
tipos de datos más sencillos. En última instancia, los átomos que van a formar esas estructuras más complejas son los tipos de datos fundamentales del
PASCAL : Byte , Integer , .... Al final, para una variable, el compilador
reservará lugar en memoria para almacenar un número determinado de bits
de información (una sucesión de ceros o unos). El tipo de dato definido para
esa variable va a determinar el modo en el que esos bits van a intervenir en
las operaciones y también el modo con en el que el programador se podrá
referir a todos esos bits de golpe o a subconjuntos de ellos.
9.6
Uniones
Comprendido lo anterior no debe resultar difı́cil entender dos posibilidades
avanzadas que permite el PASCAL en la manipulación de registros, y que
son los registros con variante ( o uniones con discriminación) y las uniones
libres. En PASCAL es posible dar una definición dinámica de la composición
de un registro. Es decir, que los campos que lo componen varı́en según el
valor de un parámetro. Para ello se utiliza la siguiente modificación de la
estructura Case dentro de la definición del registro:
Case
nombre : TipodeDato Of
caso1 : ( Especificacion1 );
caso2 : ( Especificacion2 );
.
.
.
9.6. UNIONES
121
donde nombre es el identificador elegido para la variable cuyo valor determina la composición del registro, TipodeDato el tipo de dato de la variable
nombre, caso1,caso2, ... los valores o rango de valores que darán lugar a las
distintas especificaciones del contenido del registro Especificacion1, Especificacion2,... La enumeración de las opciones no acaba con un End; por que
existe la restricción de que las variantes se han de colocar en la última parte
de la definición de un registro y por tanto acaban con el End; del fin de la
definición del registro.
Como ejemplo de la utilización de los registros con variante vamos a considerar el caso visto anteriormente de la base de datos de alumnos. Supongamos que en el campo de la nota podemos querer guardar en alguna ocasión
en vez de un dato Real una calificación global como Aprobado y elegimos
para ello un dato String. Una posibilidad es la siguiente modificación del
caso anterior:
Program Nueve;
Type
Fecha =
Record
mes : 0 .. 12; { 0 seria para indicar que no se conoce la fecha}
dia : 1 .. 31;
agno : Integer ;
End;
Alumno =
Record
nombre : String;
apellidos : String;
nacimiento : Fecha;
Case final : Boolean Of
False : ( nota : Real );
True : ( notaFinal : Real;
calificacion : String[14] );
End;
Var
uno,otro : Alumno ;
Begin
.........................................
122
CAPÍTULO 9. DATOS CON ESTRUCTURA
Instrucciones válidas de este programa serı́an tanto
uno.nota:= 6.8 ;
como,
uno.calificacion := ’Notable’;
y al programador le queda toda la responsabilidad de utilizar una u otra.
Realmente, esta estructura del PASCAL no es muy feliz en el sentido de que
el compilador no verifica el valor de la variable que da acceso a las variaciones
en los campos y su figura es meramente recordatoria para el programador.
Sea cual sea el valor de la variable utilizada para la bifurcación del tipo
de dato, se puede acceder a la información almacenada en el registro con
cualquiera de las variantes. Es responsabilidad exclusiva de programador
utilizar cada campo cuando está definido. Con las Uniones libres se hace
más explı́cito que la utilización de los campos alternativos es pura responsabilidad del programador y no se indica identificador para la variable que
gobierna las opciones. Para definir una Union Libre dentro de la definición
de la estructura Record se especifica la alternativa del modo siguiente:
Case
TipodeDato Of
caso1 : ( Especificacion1 );
caso2 : ( Especificacion2 );
.
.
.
donde caso1, caso2,... son valores posibles del tipo de dato especificado TipodeDato. Es equivalente a un Registro con variante en el que se omite el
identificador de la variable que gobierna las distintas alternativas.
Esta posibilidad de acceso variable al contenido de un registro es coherente
con lo resumido anteriormente sobre el sentido de variables y tipos de datos.
El compilador reserva para el dato en la memoria un espacio suficiente para
almacenar aquella de las variantes del registro de mayor tamaño. Se rellenará
el espacio de memoria del modo que en cada momento desee el programador y
la elección se realiza usando un campo u otro. Igualmente, la interpretación
de los bits almacenados en esas posiciones de memoria también depende
del campo del registro que se utilice, en el caso de que distintos campos
correspondan a distintos tipos de datos.
9.6. UNIONES
123
Las Uniones Libres han de usarse con precaución para evitar confusiones en la lectura de los programas, pero a veces son las adecuadas para hacer
un programa legible. En el siguiente ejemplo la unión entre el tipo de dato
Char y Byte se utiliza para escribir una función que convierte un carácter
en el correspondiente número de orden ASCII.
Program Diez;
Var
dato : Char;
Function ElAscii ( x : Char ) : Byte;
Type
Atomo =
{ Union libre de un Char con un Byte }
Record
Case Integer Of
1 : ( car : Char);
2 : ( num : Byte );
End;
Var
y : Atomo;
Begin
y.car := x;
ElAscii := y.num
End;
Begin { Diez }
Readln (dato);
Writeln (’ El caracter ’,dato , ’ corresponde al ASCII numero : ’,
ElAscii(dato) );
End. { Diez }
En la función ElAscii el tipo de dato Atomo se define como la unión libre
de un Char y un Byte. En este caso, ambos tipos de datos ocupan el
mismo espacio en memoria ( un byte ). La variable y ocupa pues un byte
124
CAPÍTULO 9. DATOS CON ESTRUCTURA
de memoria, pero la sucesión de bits que lo componen puede interpretarse o
bien como un Char o un Byte, dependiendo de como se referencie.
Capı́tulo 10
Ficheros
La entrada y salida de datos desde un programa no tiene porque realizarse
utilizando los dispositivos estandard de entrada y salida (teclado y terminal),
sino que puede realizarse a través de cualquier periférico. También es posible,
y de hecho es lo más frecuente, utilizar los dispositivos de almacenamiento
intermedio de datos. Estos dispositivos de almacenamiento de datos reciben
el nombre de archivos o ficheros. Se trata generalmente de porciones de discos
magnéticos donde se guarda información y se identifican con un nombre cuyo
formato depende del sistema operativo.
10.1
Ficheros con Tipo
Para poder referir todas las entradas y salidas, el PASCAL utiliza un tipo de
dato que se denomina File. Con este tipo de dato se pueden direccionar las
entradas y salidas de los programas a impresoras, dispositivos auxiliares,...
y también a archivos. El valor de un dato del tipo File es esencialmente
una dirección a donde se debe dirigir la UCP para transferir datos. El valor concreto es irrelevante para el programador puesto que las instrucciones
que a él se refieren nunca requieren conocerlo. El PASCAL provee un procedimiento que permite asignar a una variable del tipo File la dirección del
dispositivo de entrada o salida que el programador pretende utilizar. Este es
el procedimiento Assign que tiene dos argumentos. El primero es el nombre
de la variable definida del tipo File y el segundo es un dato tipo String que
contiene el nombre con el que el sistema operativo identifica el fichero que se
quiere utilizar. En la instrucción {3} del programa Uno se especifica que
125
126
CAPÍTULO 10. FICHEROS
con la variable almacen nos referimos al archivo que el sistema operativo
reconoce como Uno.sal . La variable almacen se ha declarado como File
of Byte. En la declaración se especifica que los datos que se van a leer o
escribir en ese dispositivo son del tipo Byte.
Program Uno;
Var
almacen : File of Byte;
num1,num2,num3,num4 : Byte;
Begin
{1} num1 := 72; num2 := 79;
{2} num3 := 76; num4 := 65;
{3} Assign(almacen,’uno.sal’);
{4} Rewrite(almacen);
{5} Write(almacen,num1,num2);
{6} Write(almacen,num3,num4);
{7} Close(almacen)
End.
El tipo de dato que va a intercambiarse con un dispositivo especificado con
una variable del tipo File puede ser tanto básico del PASCAL o definido por
el usuario. La transferencia de datos que se puede realizar es tanto entrada
como salida. Para la salida se utilizará el procedimiento Write y para la
entrada el Read. Ambos procedimientos tendrán un argumento extra, siempre el primero, que es la variable del tipo File que contiene la identificación
del periférico al que nos referimos. Por ejemplo, en la instrucción {5} se escriben los valores almacenados en num1 y num2 en el periférico identificado
con almacen, es decir en el archivo uno.sal .
En el programa Uno también aparecen las llamadas a dos procedimientos: Rewrite y Close, relacionados con las tareas que se realizan en un
ordenador para transferir datos. Con el primero se especifica que el dispositivo identificado con almacen se va a utilizar para salida de datos. Esta
orden implica la realización de tareas que dependen del tipo de dispositivo al
que nos estamos refiriendo. En el caso de archivos se ha de proceder a crear
el archivo. Cuando el dispositivo se va a utilizar como entrada de datos, las
inicializaciones necesarias se realizan con el procedimiento Reset que también tiene como argumento la variable del tipo File que se quiere inicializar.
También se utiliza Reset para salida de datos a archivos que ya existen.
10.1. FICHEROS CON TIPO
127
Cuando se acaba de transferir los datos del programa a un archivo hay
que realizar tareas simétricas a las que se realizaron en la inicialización y que
van dirigidas a liberar el archivo del control ejercido por el programa. En
la instrucción {7} del programa uno se procede a realizar esta liberación
con el procedimiento Close, que cierra el canal de comunicación abierto
anteriormente con el procedimiento Reset o Rewrite.
En todo programa que se realiza una entrada o salida de datos a un dispositivo que no es el estandard se han de incluir las ordenes correspondientes
a la asignación de dispositivo, inicialización y cierre. El esquema siempre es
Var
.
.
.
identificador File Of TipoDato
.
.
.
Begin
.
.
.
Assign( identificador, FichString);
.
.
.
Rewrite( identificador) { caso de arch. nuevo }
{ o Reset( identificador) (* caso de arch. existente *) }
.
.
.
Close( identificador)
En un mismo programa se puede leer y escribir utilizando diferentes dispositivos y el número máximo de archivos que se pueden estar utilizar al mismo
tiempo está impuesto por el sistema operativo y no por el lenguaje PASCAL
. Las palabras Rewrite y Reset tienen su origen en el uso de los dispositivos antiguos de almacenamiento intermedio de información, que todavı́a
se utilizan hoy en dı́a. Fundamentalmente se trataba de cintas magnéticas
que el operador debı́a o bien poner al principio para ser leı́das ( Reset ),
o bien poner al principio y añadir el anillo que por seguridad era necesario
para poder escribir en una cinta ( Rewrite ).
Cuando se escriben datos en un archivo, la información se escribe exactamente del mismo modo que se almacena en la memoria del ordenador. Por
ejemplo, el dato del tipo Byte 7, se escribirá como la secuencia de ceros
y unos 00000111. Por tanto, cuando se lee un dato, se pueden almacenar
directamente en memoria, sin ninguna traducción, los bits leı́dos. En la definición del identificador de la variable del tipo File, el tipo de datos que
se van a transferir se especifica, y esta información la utiliza el compilador
128
CAPÍTULO 10. FICHEROS
para verificar que no se procede a la lectura o escritura de datos de tipos distintos al anunciado en la declaración. Por supuesto, es responsabilidad del
programador leer apropiadamente los bits almacenados en un archivo. Por
ejemplo, si la información escrita en una archivo declarado como File Of
Byte se lee en otro programa distinto declarando el archivo como File Of
Char se obtendrá una traducción de números enteros a los caracteres ASCII
correspondientes. Exactamente lo contrario se consigue si el programa dos
utiliza el mismo archivo uno.sal escrito con el programa Uno como entrada
de datos.
Program Dos;
Var
almacen : File of Char;
num1,num2,num3,num4 : Char;
Begin
Assign(almacen,’uno.sal’);
Reset(almacen);
Read(almacen,num1,num2);
Read(almacen,num3,num4);
Close(almacen);
Writeln(num1,num2,num3,num4);
End.
La salida del programa dos es :
HOLA
Exactamente el mismo saludo es el que se puede leer en el fichero uno.sal
cuando se edita con un editor de ficheros ASCII como el del Entorno Integrado
de Desarrollo del TURBO PASCAL .
Un dispositivo de entrada-salida se puede definir como File Of cualquier
tipo de dato, incluyendo los definidos por el usuario. Son especialmente útiles
los archivos del tipo registro, pues en ellos se pueden almacenar fácilmente
datos con la estructura interna requerida por el usuario. Por ejemplo, en el
problema esbozado en el tema anterior sobre una base de datos con información sobre alumnos, el mantenimiento de dicha base podrı́a proceder del
siguiente modo:
Program Tres;
10.1. FICHEROS CON TIPO
129
{ Utilidad para actualizar el archivo de alumnos }
{ Solo sirve para agnadir uno nuevo
}
Type
Fecha =
Record
mes : 0 .. 12; { 0 seria para indicar que no se conoce la fecha}
dia : 1 .. 31;
agno : Integer ;
End;
Alumno =
Record
nombre : String;
apellidos : String;
nacimiento : Fecha;
End;
Var
carpeta : File of Alumno;
uno,otro : Alumno ;
respuesta,c : Char;
nomArchi : String;
Begin { Tres }
nomArchi := ’Archi.dat’;
Writeln(’Nombre : ’) ; Readln(uno.nombre);
Writeln(’Apellidos : ’); Readln(uno.apellidos);
Writeln(’A~
no de nacimiento : ’); Readln(uno.nacimiento.agno);
Writeln(’Mes de nacimiento: ’); Readln(uno.nacimiento.mes);
Writeln(’Dia de nacimiento: ’); Readln(uno.nacimiento.dia);
Writeln(’Desea incorporalo a la base de datos ? (s/n) :’);
Readln(respuesta);
If (respuesta = ’s’) Or (respuesta = ’S’) Then
Begin
Assign(carpeta,nomArchi);
Reset(carpeta);
While Not Eof(carpeta) Do Read(carpeta,otro);
Write(carpeta,uno);
Close(carpeta);
130
CAPÍTULO 10. FICHEROS
End; {endif}
End. { Tres }
En este programa se utiliza la función Eof que tiene como argumento la
variable del tipo File carpeta. Esta es una función del tipo Boolean que
devuelve el valor True si se alcanzado el fin del fichero al que apunta su
argumento. La función se utiliza en el programa Tres para recorrer todo el
archivo hasta el final y después añadir el nuevo registro.
El listado de todos los alumnos incluidos en la base de datos se podrı́a
realizar mediante el siguiente programa:
Program Cuatro;
{ Utilidad para listar el archivo de alumnos }
Type
Fecha = Record
mes : 0 .. 12;
dia : 1 .. 31;
agno : Integer ;
End;
Alumno = Record
nombre : String;
apellidos : String;
nacimiento : Fecha;
End;
Var
carpeta : File of Alumno;
uno : Alumno ;
nombreCarpeta : String;
Begin { Cuatro }
Writeln(’ Nombre del archivo donde se almacenan : ’);
Readln(nombreCarpeta);
Assign(carpeta,nombreCarpeta);
Reset(carpeta);
While Not Eof(carpeta) do begin
Read(carpeta,uno);
With uno Do
Begin
Write(apellidos,’, ’,nombre);
10.2. PROCESAMIENTO SECUENCIAL Y ALEATORIO
131
If nacimiento.mes <> 0 Then
Writeln(’
(’,nacimiento.dia,’/’,nacimiento.mes,’/’,
nacimiento.agno,’)’);
End
End; {endwhile}
Close(carpeta);
End. { Cuatro }
10.2
Procesamiento secuencial y aleatorio
Tanto en el programa Tres como en el Cuatro se está realizando un procesamiento secuencial de los archivos. Se inicia la lectura por el primer registro
y se procede hasta llegar al deseado o al final. En el caso del programa
Tres este tipo de procesamiento ha obligado, para llegar hasta el último de
los registros, a transferir a memoria el contenido de todos los registros. La
estructura interna de un archivo es lineal y muy parecida a la estructura
Array del PASCAL . Por tanto, deberı́a ser posible acceder a los diferentes
registros sin necesidad de leerlos a la memoria RAM. Esta posibilidad es la
que permite el procedimiento del PASCAL Seek que tiene dos argumentos:
el primero un dato tipo File, y el segundo LongInt. El efecto de esta función es preparar el archivo identificado por el dato File para la lectura o
escritura a partir del número de registro que contiene la variable LongInt.
De hecho, dado que los registros se empiezan a contar con el 0, se colocará
pasado el registro indicado por el número LongInt. Por ejemplo, si fichero
se define como File Of Integer, la llamada Seek (fichero, 23) hará que
la siguiente lectura realizada sobre fichero transfiera a la memoria del ordenador el dato Integer con el número de orden 24; se han saltado 46 bytes
del fichero.
Con la posibilidad brindada por el procedimiento Seek se puede realizar
lo que se llama procesamiento aleatorio de ficheros. A pesar del nombre,
esto no tiene nada que ver con el azar, sino que implica leer o escribir en
archivos en un orden distinto que el secuencial. Para explicar la utilidad y
servidumbres de este tipo de procesamiento, vamos a modificar el diseño de
la base de datos de alumnos sugerida anteriormente. El objetivo es colocar
al principio del fichero un número que nos indique la cantidad de alumnos
incluidos en la lista.
Program IniciaLista;
132
CAPÍTULO 10. FICHEROS
{ Utilidad para inicializar una lista de alumnos }
Type
Fecha =
Record
mes : 0 .. 12; dia : 1 .. 31; agno : Integer ;
End;
Alumno =
Record
Case Boolean of
True : ( nombre : String;
apellidos : String;
nacimiento : Fecha );
False :
( numeroTotal : Integer );
End;
Var
carpeta : File of Alumno;
otro : Alumno ;
nomArchi : String;
Begin { IniciaLista }
Writeln(’Inicialización de un archivo de alumnos’);
Writeln(’Nombre del archivo: ’); Readln(NomArchi);
Assign(carpeta,nomArchi);
Rewrite(carpeta); {Se crea el archivo}
otro.numeroTotal := 0;
{Se inicializa a 0 el numero de orden }
Write(carpeta,otro);
Close(carpeta);
End. { IniciaLista }
En el programa IniciaLista se procede a inicializar la base de datos: se
crea el archivo con el nombre indicado por el usuario y se escribe en el
primer registro el Integer 0 para indicar que no hay todavı́a ningún alumno
incluido. Como almacen se tiene que definir como un File Of alumno para
poder escribir un entero en el primer registro tenemos que recurrir a una
unión libre del registro.
El programa que puede añadir un alumno en la base de datos es :
Program AumentaLista;
10.2. PROCESAMIENTO SECUENCIAL Y ALEATORIO
133
{ Utilidad para agnadir una alumno a una lista }
{ Solo sirve para agnadir UNO }
Type
Fecha =
Record
mes : 0 .. 12; dia : 1 .. 31; agno : Integer ;
End;
Alumno =
Record
Case Boolean of
True : ( nombre : String;
apellidos : String;
nacimiento : Fecha );
False : ( numeroTotal : Integer );
End;
Var
carpeta : File of Alumno;
uno,otro : Alumno ;
respuesta : Char;
nomArchi : String;
Begin {AumentaLista}
Writeln(’Nombre del archivo: ’); Readln(NomArchi);
Writeln(’Nombre : ’) ; Readln(uno.nombre);
Writeln(’Apellidos : ’); Readln(uno.apellidos);
Writeln(’A~
no de nacimiento : ’); Readln(uno.nacimiento.agno);
Writeln(’Mes de nacimiento: ’); Readln(uno.nacimiento.mes);
Writeln(’Dia de nacimiento: ’); Readln(uno.nacimiento.dia);
Writeln(’Desea incorporarlo a la base de datos ? (s/n) :’);
Read(respuesta);
If (respuesta = ’s’) Or (respuesta = ’S’) Then
Begin
Assign(carpeta,nomArchi);
Reset(carpeta);
Read(carpeta,otro);
Seek(carpeta,otro.numeroTotal+1); {Al final}
Write(carpeta,uno);
134
CAPÍTULO 10. FICHEROS
Seek(carpeta,0); { Al principio }
otro.numeroTotal := otro.numeroTotal + 1;
Write(carpeta,otro);
Close(carpeta);
End; {EndIf}
End. {AumentaLista}
En este programa, para llegar al final del fichero simplemente se saltan los
registros indicados al principio, y se escribe el nuevo. Después, se vuelve al
principio para aumentar en 1 el contador de registros almacenados.
El listado de los alumnos en la base de datos serı́a :
Program ListaLista;
{ Utilidad para escribir el archivo de alumnos }
Type
Fecha =
Record
mes : 0 .. 12; dia : 1 .. 31; agno : Integer ;
End;
Alumno =
Record
Case Boolean of
True : ( nombre : String;
apellidos : String;
nacimiento : Fecha );
False :
( numeroTotal : Integer );
End;
Var
i : Integer;
carpeta : File of Alumno;
uno,n : Alumno ;
nombreCarpeta : String;
Begin { ListaLista }
Writeln(’ Nombre del archivo donde se almacenan : ’);
Readln(nombreCarpeta);
Assign(carpeta,nombreCarpeta);
Reset(carpeta);
Read(carpeta,n);
10.2. PROCESAMIENTO SECUENCIAL Y ALEATORIO
135
For i := 1 To n.numeroTotal Do
Begin
Read(carpeta,uno);
With uno Do
Begin
Write(i:3,’ ’,apellidos,’, ’,nombre);
If nacimiento.mes <> 0 Then
Writeln(’
(’,nacimiento.dia,’/’,nacimiento.mes,’/’,
nacimiento.agno,’)’);
End
End; {endfor}
Close(carpeta);
End. { ListaLista }
Con esta nueva definición de la base de datos de alumnos resulta muy sencillo escribir un programa para corregir errores en alguno de los registros
almacenados.
Program ModificaLista;
{ Utilidad para modificar un alumno en la lista }
Type
Fecha =
Record
mes : 0 .. 12; dia : 1 .. 31; agno : Integer ;
End;
Alumno =
Record
Case Boolean of
True : ( nombre : String;
apellidos : String;
nacimiento : Fecha );
False : ( numeroTotal : Integer );
End;
Var
elemento :Integer;
carpeta : File of Alumno;
uno,otro : Alumno ;
136
CAPÍTULO 10. FICHEROS
Respuesta : Char;
nomArchi : String;
Begin {ModificaLista}
Writeln(’Nombre del archivo: ’); Readln(NomArchi);
Writeln(’Numero de orden: ’); Readln(elemento);
Writeln(’Nombre : ’) ; Readln(uno.nombre);
Writeln(’Apellidos : ’); Readln(uno.apellidos);
Writeln(’A~
no de nacimiento : ’); Readln(uno.nacimiento.agno);
Writeln(’Mes de nacimiento: ’); Readln(uno.nacimiento.mes);
Writeln(’Dia de nacimiento: ’); Readln(uno.nacimiento.dia);
Writeln(’Desea incorporalo a la base de datos ? (s/n) :’);
Read(respuesta);
If (respuesta = ’s’) Or (respuesta = ’S’) Then
Begin
Assign(carpeta,nomArchi);
Reset(carpeta);
Read(carpeta,otro);
Seek(carpeta,elemento);
Write(carpeta,uno);
Close(carpeta);
End; {EndIf}
End. {ModificaLista}
En este programa se accede directamente al registro que se quiere modificar
y el usuario lo identifica con el número de orden que aparece en el listado.
Esta labor hubiera sido mucho más difı́cil de realizar con un procesamiento
meramente secuencial del archivo.
10.3
Ficheros de Texto
Como se mencionó anteriormente, el uso de ficheros de un tipo de dato definido, permite que la información se almacene en los archivos del mismo
modo que se escribe en la memoria del ordenador. Por tanto, no es necesario
ninguna traducción para pasarlos a la memoria RAM. La ventaja de usar
este tipo de archivos es la rapidez en la transferencia de información entre
memoria RAM y dispositivos de almacenamiento. El inconveniente es la falta
10.3. FICHEROS DE TEXTO
137
de compatibilidad: para leer correctamente la secuencia de bits escrita por
un programa PASCAL en un fichero con tipo es necesario u otro programa
PASCAL o un programa muy sutil en otro lenguaje que tenga en cuenta
la estructura interna de los datos PASCAL . El modo estándar actual para
transferir información es utilizar el código de caracteres ASCII almacenados
en un byte con el bit menos significativo a la derecha. Por tanto, si se quiere
escribir en un archivo información que sea legible por la mayorı́a de las utilidades que existen comúnmente hoy en dı́a hay que utilizar los datos Char
del PASCAL . Además también es estándar el modo en el que se especifica
el salto de lı́nea.
El PASCAL suministra el tipo de fichero llamado Text para definir dispositivos externos a los que se transfiere toda la información traducida a
caracteres ASCII. Por ejemplo, los dispositivos estándar de entrada y salida (teclado y terminal TRC) son un ejemplo de Text. Los procedimientos
Readln y Writeln, que no se podı́an utilizar con los ficheros con tipo se
pueden utilizar con los ficheros de texto y son los que se encargan de gestionar los caracteres ASCII de cambio de lı́nea (códigos 10 y 13 del ASCII).
Un fichero Text es algo más que un File Of Char puesto que el compilador se encarga de traducir las ordenes de escritura de todo tipo de datos a
caracteres ASCII y con los formatos que indique el programador.
El programa siguiente:
Program Cinco;
Var
almacen : Text;
saludo : String;
Begin
Assign(almacen,’uno.sal’);
Reset(almacen);
Readln(almacen,saludo);
Writeln(saludo);
Close(almacen)
End.
tiene la misma salida al terminal que el programa Dos cuando el archivo
Uno.sal es el escrito por el programa Uno. Ejemplos de manipulación de
ficheros con texto son todos los programas vistos hasta este tema puesto que
toda la entrada y salida de datos se realizaba sobre los dispositivos Input y
Output que son ficheros del tipo Text.
138
CAPÍTULO 10. FICHEROS
La ventaja de portabilidad de los ficheros del tipo Text es a costa de
tiempo de procesamiento y espacio de disco. Antes de pasar un dato de la
memoria a un archivo o viceversa se ha de realizar la traducción de caracteres
ASCII. Con los dos siguientes programas:
Program Seis;
Uses Dos;
Var
almacen
: File Of Real;
x : Real;
i : Integer;
h1,m1,s1,c1,h2,m2,s2,c2 : Word;
Begin
Assign(almacen,’seis.sal’);
Rewrite(almacen);
GetTime(h1,m1,s1,c1);
Writeln(h1,’:’,m1,’:’,s1,’:’,c1);
For i := 1 to 10000 Do Begin
x := Random;
Write(almacen,x);
End; {endfor}
GetTime(h2,m2,s2,c2);
Writeln(h2,’:’,m2,’:’,s2,’:’,c2);
Close(almacen)
End.
y,
Program siete;
Uses Dos;
Var
almacen
: Text;
x : Real;
i : Integer;
h1,m1,s1,c1,h2,m2,s2,c2 : Word;
Begin
Assign(almacen,’siete.sal’);
Rewrite(almacen);
GetTime(h1,m1,s1,c1);
10.3. FICHEROS DE TEXTO
139
Writeln(h1,’:’,m1,’:’,s1,’:’,c1);
For i := 1 to 10000 Do Begin
x := Random;
Writeln(almacen,x);
End; {endfor}
GetTime(h2,m2,s2,c2);
Writeln(h2,’:’,m2,’:’,s2,’:’,c2);
Close(almacen)
End.
se puede comprobar la diferencia entre los dos tipos de transferencia de información. En el Seis se escriben los datos Real en un fichero File Of
Real y en el Siete en un fichero Text. El procedimiento GetTime, que se
encuentra en la unidad Dos, devuelve el tiempo en horas, minutos, segundos
y centésimas. Este procedimiento es el que se utiliza en ambos casos para
detectar el tiempo que se emplea en escribir 1000 datos Real. Con la salida
de estos dos programas se puede comprobar que el segundo programa emplea
casi el doble de tiempo que el primero en escribirlos. Ası́ mismo, el archivo
Siete.sal que se crea ocupa casi tres veces el espacio ocupado por el fichero
Seis.sal. Este último, sólo ocupa 60000 bytes puesto que cada Real está
formado por 6 bytes, y Siete.sal ocupa 190000 bytes puesto que se necesitan
19 caracteres ASCII para describir con el formato estandard cada dato Real
(17 para el número y 2 para el salto de lı́nea).
Otra desventaja adicional de los ficheros de texto es que sólo aceptan
procesamiento secuencial y NO aleatorio.
140
CAPÍTULO 10. FICHEROS
Capı́tulo 11
Punteros y asignación dinámica
de memoria
11.1
Contenidos, direcciones e identificadores
Hasta ahora hemos considerado que los datos almacenados en la memoria
pueden accederse a través del nombre de variables y constantes. Asumimos
que el nombre de la variable es el indicador que permite al compilador determinar a qué dato nos referimos y generar código UCP para operar con él.
Cada dato está almacenado en una posición de memoria y su contenido se
puede modificar a través de la relación unı́voca que existe entre el nombre
de la variable y la posición que el dato ocupa en la memoria del ordenador.
Consideremos un programa tan sencillo como el siguiente:
{1}
{2}
{3}
Program Uno;
Var
a , b : Integer;
Begin { Uno }
Readln(a);
b := a;
Writeln(a:4,’ ’,b:4);
End. { Uno }
en el que se definen las variables a y b para almacenar datos del tipo Integer. En la instrucción {3} se especifica que el dato almacenado en la
141
142
CAPÍTULO 11. PUNTEROS
posición de memoria a la que nos referimos con la variable a ha de almacenarse también en la posición de memoria a la que nos referimos con la
variable b.
11.2
Punteros
En ningún momento se ha hecho explicito en la sintaxis del lenguaje que un
dato está identificado internamente por una posición de memoria. Sin embargo, es muy útil poder manipular los datos realizando algoritmos en los que
también interviene la dirección de los datos. Para ello el PASCAL permite
que en los programas aparezca un tipo de dato que son las direcciones de los
datos. Estas direcciones se llaman normalmente punteros, y en un programa
PASCAL pueden intervenir las direcciones de datos que estén definidos. Se
especifica que una variable es un puntero a un tipo de dato con la ayuda del
calificador ˆ que se antepone a un identificador.
El dato ^tipodato es el puntero a un dato del tipo tipodato. Por
ejemplo en el segmento de programa :
Type
Fecha =
Record
dia : Integer;
mes : Integer;
End;
Var
a: Integer;
px : ^Real;
pa : ^Integer;
pHoy,pAyer : ^Fecha;
.
.
.
se está declarando que px es un puntero a un dato Real, pa un puntero
a un Integer, y pHoy y pAyer punteros a registros de Fecha. El mismo
calificador ˆ sirve para especificar que queremos utilizar el dato almacenado
en la dirección de memoria especificada por el puntero, pero esta vez se añade
al final del identificador. Por ejemplo, cuando aparece en una parte posterior
del programa citado anteriormente pHoy^
estamos indicando el contenido
del registro del tipo fecha apuntado por el puntero pHoy. La asignación
11.2. PUNTEROS
143
.
.
pHoy^ := pAyer^;
.
.
.
.
implica que se almacene en el lugar de memoria indicado por pHoy el dato
almacenado en el registro al que apunta pAyer. Para completar la herramienta es necesario un operador que sea capaz de extraer de una variable su
dirección, y este es el operador que se representa por @. La asignación:
pa := @a;
es correcta porque pa es el puntero a un Integer y el resultado de operar @
sobre a (que es un Integer ) es también el puntero a un Integer.
Una versión sofisticada del programa Uno es la siguiente:
{1}
{2}
{3}
{4}
Program Dos;
Var
a ,b : Integer;
pb : ^Integer;
Begin { Dos }
a := 10 ;
pb := @a ;
b := pb^ ;
Writeln(a:4,’ ’,b:4);
End. { Dos }
en la que se obtiene exactamente el mismo resultado que en Uno pero con
el uso explı́cito de direcciones de variables. La variable pb es un puntero a
un Integer, es decir la dirección de un dato Integer, y en la instrucción
{2} se determina que es exactamente la misma que la de la variable Integer
a. En la instrucción {3} se asigna a la variable b el dato almacenado en
la dirección que habı́amos almacenado en el dato ( puntero ) pb. El dato
almacenado en b será pues el mismo que el almacenado en a. Las instrucciones {3} y {4} de programa Dos producen el mismo resultado que la {2}
del programa Uno recorriendo un camino más largo y con una sintaxis menos clara. Pero si este tipo nuevo de dato se introduce en el lenguaje no es
para hacer complicado lo sencillo, sino para aumentar las posibilidades del
lenguaje tal como comentaremos en este tema.
Antes de seguir adelante puede ser ilustrativo considerar el siguiente programa,
144
CAPÍTULO 11. PUNTEROS
Program Tres;
Var
a
: String;
pa,pb : ^String;
Begin { Tres }
a := ’Hola’;
{ en a se almacena ’Hola’ }
pa := @a;
{ pa apunta a}
pb:= pa;
{ pb apunta a}
writeln(a);
pb^ := ’Adios’;
Writeln(a); { el contenido de a es ’Adios’ }
End. { Tres }
en el que se modifica el valor del String a indirectamente a través del contenido del puntero pb. Las direcciones de memoria que se almacenan en
los datos del tipo puntero, dependerán de cada tipo de ordenador, y en general, el programador puede olvidarse de esos detalles, pues sólo utilizará
los punteros para manipular los datos a los que apuntan y evitará asignar
directamente valores numéricos a los punteros. Generalmente se asigna a un
puntero el contenido de otro. Hay una excepción: se trata del valor Nil, ya
que no corresponde a un lugar de memoria y se utiliza para identificar que un
puntero no apunta a ningún lugar de la memoria. A un puntero, apuntando
a cualquier tipo de dato, siempre se le puede asignar el valor Nil.
Las operaciones que se pueden realizar con los punteros, son asignar y
comparar por igualdad o desigualdad. Con estas comparaciones se puede
discernir la equivalencia de direcciones. Sin embargo, no se pueden comparar
los punteros con los operadores > y <.
Vamos a verificar el motivo de llamar argumentos por referencia o dirección a los argumentos antecedidos por Var en las funciones y procedimientos.
La salida del programa Cuatro,
Program Cuatro;
Type pinte = ^Integer;
Var
a ,b ,c: Integer;
11.3. ASIGNACIÓN DINÁMICA DE MEMORIA.
145
Procedure Cuacua (
x : pinte;
y : Integer;
Var z : Integer
);
Begin
x^ := x^ * x^;
y := y * y ;
z := z * z ;
End;
Begin { Cuatro }
a := 2 ;
b := 3 ;
c := 4 ;
Cuacua(@a,b,c);
Writeln(a:6,’ ’,b:6,’
End. { Cuatro }
’,c:6);
es,
4
3
16
El valor de la variable b no se modifica como resultado de la llamada a la
función porque es una argumento pasado por contenido. La variable c se
modifica porque se pasa por referencia, y la variable a también modifica su
valor porque el argumento es la dirección de a y por tanto en la función de
puede modificar el contenido de esa dirección.
11.3
Asignación dinámica de memoria.
Una utilidad de los punteros es poder crear programas que ocupen la cantidad de memoria del ordenador adecuada a cada caso según se determina
durante la ejecución del programa. Cuando un programa se va a ejecutar, se
carga en la memoria del ordenador y la memoria ocupada por el programa
está estructurada en varias partes o segmentos. El código para la UCP se
carga en memoria en el llamado segmento de código. Los datos definidos
en el programa se cargan en el llamado segmento de datos, y hay un tercer
segmento llamado pila (stack) que se reserva para los datos que se han de
146
CAPÍTULO 11. PUNTEROS
manipular temporalmente durante la ejecución del programa, por ejemplo,
para almacenar los valores de las variables que se han de crear para realizar un procedimiento. El resto de la memoria se gestiona como un montón
(heap) y también puede estar disponible para el programa.
La función New es la que gestiona la captación para el programa de la
memoria del montón. El argumento de la función ha de ser un puntero a
un tipo de dato del PASCAL o definido por el programador, y la función
New asigna a ese puntero el valor de una dirección de memoria del montón
con el tamaño adecuado para almacenar un dato del tipo al que apunta
su argumento. En el momento en el que esa porción de memoria ya no
sea necesaria para el algoritmo codificado en el programa, puede liberarse
del control del programa con la función Dispose, que tiene por argumento
el puntero donde se almacena la dirección de la porción de memoria que
se quiere liberar. A ésta gestión de la memoria del ordenador durante la
ejecución del programa se le llama asignación dinámica de la memoria.
El programa Cinco es una modificación del Tres en el que la memoria
utilizada para almacenar los saludos no está en el segmento de datos sino en
el Heap.
Program Cinco;
Var
pa,pb : ^String;
Begin { Cinco }
New(pa);
pa^ := ’Hola’;
pb:= pa;
writeln(pa^);
pb^ := ’Adios’;
Writeln(pa^);
Dispose(pb);
End. { Cinco }
Se reserva la memoria con New antes de almacenar ningún valor en esa
dirección. La última instrucción hubiera igualmente podido ser Dispose(pa)
porque ambos punteros almacenan la misma dirección de memoria que se
libera con Dispose. Es más, la siguiente modificación es incorrecta por
varios motivos.
11.4. DECLARACIONES RECURSIVAS DE TIPOS DE DATOS
147
Program CincoIncorrecto;
Var
pa,pb : ^String;
Begin { Incorrecto }
New(pa);
pa^ := ’Hola’;
New(pb);
pb:= pa;
writeln(pa^);
pb^ := ’Adios’;
Writeln(pa^);
Dispose(pb);
Dispose(pa);
End. { Incorrecto }
Primero, el programa darı́a un error en la ejecución. La última instrucción
intenta liberar un lugar de memoria que ya ha sido liberado en la llamada anterior a Dispose. Por otra parte, es un ejemplo claro de mal uso de la gestión
dinámica de memoria. La memoria reservada para el programa mediante la
tercera instrucción (256 bytes) no se puede liberar puesto que hemos perdido
la pista de cuál era. Es una práctica poco recomendable escribir programas
que cuando acaban dejan sin liberar memoria que reservaron del montón.
11.4
Declaraciones recursivas de tipos de datos
La asignación dinámica de memoria permite crear estructuras de datos que
pueden crecer y disminuir según los requerimientos que el programa detecta
durante la ejecución. La más sencilla de ellas es la llamada lista unida. Mediante definición de punteros a datos en la memoria y uniéndolos entre ellos,
se obtienen listas de objetos sin tamaño especı́fico. Pueden ser listas vacı́as
o contener miles de datos, crecer y menguar durante la ejecución del programa. Son estructuras de datos recursivas que se comprenden fácilmente con
esquemas, pero primero veremos un ejemplo de como se definen en PASCAL
.
Type
148
CAPÍTULO 11. PUNTEROS
Enlace = ^Nodo;
Nodo =
Record
clave : Integer;
siguiente : Enlace
End;
Primero se define el puntero Enlace como la dirección a un dato (todavı́a
sin definir) Nodo y después se declara cual es el significado del tipo de dato
Nodo. Este es un registro que en su primer campo puede almacenar un
entero y en el segundo un puntero, Enlace, a un dato del mismo tipo Nodo.
La recursividad está obviamente en la autorreferencia durante la definición.
Merece la pena destacar que el caso de los punteros es único en el PASCAL
en cuanto a que es posible definir un tipo de dato a partir de otro que
todavı́a no está definido. Esta flexibilidad es necesaria para poder generar la
recursividad.
Las variables del tipo Nodo van a ser los ladrillos con los que se construirá
una lista unida de números enteros. Cada uno de ellos puede almacenar un
número entero y la dirección del siguiente en la lista.
La lista más pequeña que podemos tener es de un sólo elemento. Para
identificar que nadie le sigue podemos recurrir al valor Nil.
Enlace
Nodo
-Nodo
-Nodo
HH
HH
j
Nil
El dibujo es muy ilustrativo de la estructura de datos llamada lista unida.
Cada elemento de la lista puede almacenar información relevante para el
11.4. DECLARACIONES RECURSIVAS DE TIPOS DE DATOS
149
problema en la que se utiliza y también el enlace con el siguiente vagón.
Para cada elemento de la lista habrá que reservar memoria, tanto para la
información que se quiere guardar, como para el puntero que sirve de enlace.
Vamos a utilizar en el programa Seis una lista unida para almacenar
en la memoria del ordenador cuantos números enteros teclee el usuario para
poder luego listarlos en el orden inverso al que se teclearon. La memoria del
ordenador que necesitará el programa Seis para ejecutarse dependerá de los
números que se tecleen. Si son unos pocos será muy poca la memoria que
tomará, y, el lı́mite superior de números que se pueden teclear dependerá de
la memoria RAM instalada en el ordenador. Si en vez de una lista unida
hubiéramos utilizado una estructura Array para almacenar los enteros, la
memoria deberı́a determinarse al escribir en el programa el rango de variación
del ı́ndice del Array.
Program Seis;
Type
enlace = ^Nodo;
Nodo =
Record
clave : Integer;
siguiente : Enlace
End;
Var
y,z : enlace;
i : Integer;
Begin
{ Se inicializa la lista }
New(z);
z^.siguiente := Nil;
{El ultimo
elemento apunta a Nil}
{Se leen los datos y se incluyen en la lista }
Readln(i);
While i >= 0 Do
{ Un numero negativo finaliza el proceso}
Begin
New(y);
y^.clave := i;
150
CAPÍTULO 11. PUNTEROS
y^.siguiente := z;
z := y;
Readln(i)
End;
{Se escribe la lista}
While (y^.siguiente <> Nil ) Do
Begin
Writeln(y^.clave) ;
z := y^.siguiente;
Dispose(y);
y := z;
End;
Dispose(z)
End.
Para detectar el final de la lista, recurrimos a la dirección Nil. Cuando recorremos la lista para escribir el contenido, nos detenemos cuando un nodo (el
último) apunta a esa constante. No hay que olvidar nunca reservar memoria
del Heap para cada elemento nuevo de la lista. De lo contrario, se pueden
obtener resultados impredecibles. Cuando se escribe información en la dirección de memoria dictada por un puntero que no ha sido convenientemente
inicializado estaremos escribiendo en un lugar descontrolado y los resultados
son imprevisibles. Dado que los números tecleados por el usuario se van incorporando a la lista según se teclean, y no guardamos la pista sobre dónde está
el primer elemento almacenado en memoria, sólo podemos listar los números
en el orden inverso al que fueron tecleados. Para listar los números en el
mismo orden en el que fueron tecleados existen varias posibilidades pero su
análisis queda fuera de los objetivos de este curso. Sin ahondar mucho más
en las estructuras de datos que se pueden generar con los punteros merece la
pena resaltar la simplicidad y eficiencia de cierta operaciones que se realizan
comúnmente sobre listas unidas. El insertar un nuevo elemento en el lugar
elegido de la lista puede ser algo tan sencillo como :
{un nuevo nodo se crea a continuacion del apuntado por y}
New(z);
z^.clave := i;
z^.siguiente := y^.siguiente;
y^.siguiente := z;
11.4. DECLARACIONES RECURSIVAS DE TIPOS DE DATOS
151
y la eliminación de un nodo :
{Se elimina el nodo a continuacion del apuntado por y}
z:= y^siguiente;
y^.siguiente := y^.siguiente^.siguiente ;
Dispose(z);
Estructuras de datos como listas doblemente unidas, circulares y árboles, son
muy comunes en la realización de algoritmos eficientes.
A estas alturas del curso podemos entender perfectamente el programa
llamado Josefo que se estudió en el Tema 3. No obstante, el alumno no
debe esperar que con los conocimientos sobre programación adquiridos hasta
ahora deberı́a haber diseñado un programa similar a Josefo para resolver
el problema. Tales diseños no aparecen generalmente por intuición sino como resultado del estudio de las estructuras de datos y algoritmos que se
han desarrollado para diferentes problemas tipo planteados a la ciencia de
la computación. El objetivo de esta parte del curso era que el alumno comprendiera un programa como el citado y sobre todo que en los programas que
escriba, sean los problemas que resuelvan más o menos sencillos, utilice la
herramientas aprendidas siguiendo las normas de claridad y estilo referidas
continuamente.
Descargar