Algoritmos y Programación en Pascal
Algoritmos y Programación en Pascal
Algoritmos y
Programación
Índice
luiscastellanos@[Link]
Cinta magnética
Disco magnético
Conector de pagina
Líneas de flujo
Anotación
• Los Diagramas de flujo deben escribirse de arriba hacia abajo, y/o de izquierda
a derecha.
• Los símbolos se unen con líneas, las cuales tienen en la punta una flecha que
indica la dirección que fluye la información procesos, se deben de utilizar
solamente líneas de flujo horizontal o verticales (nunca diagonales).
• Se debe evitar el cruce de líneas, para lo cual se quisiera separar el flujo del
diagrama a un sitio distinto, se pudiera realizar utilizando los conectores. Se
debe tener en cuenta que solo se vana utilizar conectores cuando sea
estrictamente necesario.
• No deben quedar líneas de flujo sin conectar
• Todo texto escrito dentro de un símbolo debe ser legible, preciso, evitando el
uso de muchas palabras.
• Todos los símbolos pueden tener más de una línea de entrada, a excepción del
símbolo final.
• Solo los símbolos de decisión pueden y deben tener más de una línea de flujo
de salida.
1.4 Ejemplos:
Los lenguajes de programación de hoy en día, son el producto del desarrollo que
empezó en los años 50. Muchos conceptos acerca de lenguajes han sido
inventados, probados y mejorados al ser incorporados en lenguajes sucesivos. Sin
contar con algunas pocas excepciones, el diseño de cada lenguaje ha sido
fuertemente influenciado con la experiencia de los lenguajes primitivos.
El más primitivo de los lenguajes de alto nivel fue Fortran, y también Cobol. Fortran
nos mostró el uso de expresiones simbólicas y subprogramas con parámetros. Cobol
introdujo el concepto de descripción de datos.
Fortran y Algol60 fueron mucho más útiles para cálculos numéricos computacionales;
y Cobol para procesamiento de datos comerciales; por otro lado PL/I fue el primer
intento de diseño de un lenguaje de propósito general, lo cual se realizó mezclando
diferentes características de los tres primeros lenguajes primitivos. También introdujo
formas de excepción de bajo nivel, concurrencia y muchas otras características. El
lenguaje resultante fue gigante, complejo, incoherente y complicado de implementar.
La experiencia con PL/I mostró la ineficacia de usar una simple recopilación de
características para implementar un lenguaje de programación poderoso y de
propósito general.
El más poderoso sucesor del Pascal fue el lenguaje ADA. Este lenguaje introdujo
paquetes diseñados para ayudar a la construcción de largos programas modulares,
así como también introdujo formas de excepciones de alto nivel y concurrencia.
Al igual que PL/I, ADA fue forjado por sus diseñadores para llegar a ser el lenguaje
de programación de propósito general estándar, pero a pesar de ello, recibió muchas
críticas.
2.5 Compilación.
• La mayoría de los lenguajes actuales son compiladores, y suelen incluir:
o Un editor para escribir o revisar los programas.
o El compilador propiamente dicho, que los convierte a código máquina.
o Otros módulos auxiliares, como enlazadores (linkers) para unir
distintos subprogramas, y depuradores (debuggers) para ayudar a
descubrir errores.
• El programa escrito en un lenguaje de programación (comprensible por el ser
humano, aunque se suelen corresponder con lenguajes formales descritos
por gramáticas independientes del contexto) no es inmediatamente
ejecutado en una computadora. La opción más común es compilar el
programa, aunque también puede ser ejecutado mediante un intérprete
informático.
• El código fuente del programa se debe someter a un proceso de
transformación para convertirse en lenguaje máquina, interpretable por el
procesador. A este proceso se le llama compilación.
• Normalmente la creación de un programa ejecutable (un típico .exe para
Microsoft Windows) conlleva dos pasos. El primer paso se llama compilación
(propiamente dicho) y traduce el código fuente escrito en un lenguaje de
programación almacenado en un archivo a código en bajo nivel,
(normalmente en código objeto no directamente al lenguaje máquina). El
segundo paso se llama enlazado (del inglés link o linker) se junta el código
de bajo nivel generado de todos los ficheros que se han mandado compilar y
se añade el código de las funciones que hay el las bibliotecas del compilador
para que el ejecutable pueda comunicarse con el sistemas operativo y
traduce el código objeto a código máquina.
• Estos dos pasos se pueden mandar hacer por separado, almacenando el
resultado de la fase de compilación en archivos objetos (un típico .obj para
Microsoft Windows, .o para Unix), para enlazarlos posteriormente, o crear
directamente el ejecutable con lo que la fase de compilación se almacena
sólo temporalmente.
• Un programa podría tener partes escritas en varios lenguajes (generalmente
C, C++ y Asm), que se podrían compilar de forma independiente y enlazar
juntas para formar un único ejecutable.
program Saludo;
begin
write('Hola');
end.
program Saludo(output);
Aunque no sea necesaria la línea de "program", su empleo puede resultar cómodo
si se quiere poder recordar el objetivo del programa con sólo un vistazo rápido a su
cabecera.
Saludo es un identificador que nos va a servir para indicar el nombre del programa.
Los "identificadores" son palabras que usaremos para referirnos a una variable, una
constante, el nombre de una función o de un procedimiento, etc.
Una variable equivale a la clásica incógnita "x" que todos hemos usado en
matemáticas, que puede ser cualquier número. Ahora nuestras "incógnitas" podrán
tener cualquier valor (no sólo un número: también podremos guardar textos, fichas
sobre personas o libros, etc.) y nombres más largos (y que expliquen mejor su
contenido).
Estos nombres de "identificadores" serán combinaciones de letras y números, junto
con algunos (pocos) símbolos especiales, como el de subrayado (_). No podrán
empezar con un número, sino por un carácter alfabético (A a Z, sin Ñ ni acentos) o un
subrayado, y no podrán contener espacios.
Así, serían identificadores correctos: Nombre_De_Programa, programa2,
_SegundoPrograma pero no serían admisibles 2programa, 2ºprog, tal&tal, Prueba de
programa, ProgramaParaMí (unos por empezar por números, otros por tener
caracteres no aceptados, y otros por las dos cosas).
Las palabras "begin" y "end" marcan el principio y el final del programa, que esta
vez sólo se compone de una línea. Nótese que, como se dijo, el último "end" debe
terminar con un punto.
"Write" es la orden que permite escribir un texto en pantalla. El conjunto de todo lo
que se desee escribir se indica entre paréntesis.
Cuando se trata de un texto que queremos que aparezca "tal cual", éste se encierra
entre comillas (una comilla simple para el principio y otra para el final, como aparece en
el ejemplo).
El punto y coma que sigue a la orden "write" no es necesario (va justo antes de un
"end"), pero tampoco es un error; así que podemos dejarlo, por si después añadimos
otra orden entre "write" y "end".
La orden "write" aparece algo más a la derecha que el resto. Esto se llama escritura
indentada, y consiste en escribir a la misma altura todos los comandos que se
encuentran a un mismo nivel, algo más a la derecha los que están en un nivel inferior,
y así sucesivamente. Se irá viendo con más detalle a medida que se avanza.
En un programa en Pascal no hay necesidad de conservar una estructura tal que
aparezca cada orden en una línea distinta. Se suele hacer así por claridad, pero
realmente son los puntos y coma (cuando son necesarios) lo que indica el final de una
orden, por lo que el programa anterior se podría haber escrito:
program Saludo; begin write('Hola') end.
Una última observación: si se compila este programa desde Turbo Pascal 5.0 o una
versión superior, aparentemente "no pasa nada". No es así, sino que se ejecuta y se
vuelve al editor tan rápido que no nos da tiempo a verlo. La solución es pulsar Alt+F5
para que nos muestre la pantalla del DOS.
Otro ejemplo:
FUNCTION PreguntaNombre:STRING;
VAR
nombre:STRING;
BEGIN
REPEAT
WRITELN('Ingresa tu nombre:');
READLN(nombre);
UNTIL nombre <> '';
PreguntaNombre := nombre;
END;
PROCEDURE Saluda;
VAR
nombre:STRING;
BEGIN
nombre := PreguntaNombre;
IF nombre = 'wirth' THEN
WRITELN('Hola señor Wirth!');
ELSE
WRITELN('Hola '+nombre);
END;
BEGIN
Saluda;
END.
program Saludo2;
var
nombre: string;
begin
writeln('Introduce tu nombre, por favor');
readln(nombre);
write('Hola ',nombre);
end.
Aquí ya aparecen más conceptos nuevos. En primer lugar, hemos definido una
variable, para lo que empleamos la palabra var, seguida del nombre que vamos a dar a
la variable, y del tipo de datos que va a almacenar esa variable.
Los nombres de las variables siguen las reglas que ya habíamos mencionado para
los identificadores en general.
Con la palabra string decimos que la variable nombre va a contener una cadena de
caracteres (letras o números). Un poco más adelante, en esta misma lección,
comentamos los principales tipos de datos que vamos a manejar.
Pasemos al cuerpo del programa. En él comenzamos escribiendo un mensaje de
aviso. Esta vez se ha empleado "writeln", que es exactamente igual que "write", con la
única diferencia de que después de visualizar el mensaje, el cursor (la posición en la
que se seguiría escribiendo, marcada normalmente por una rayita o un cuadrado que
parpadea) pasa a la línea siguiente, en vez de quedarse justo después del mensaje
escrito.
Después se espera a que el usuario introduzca su nombre, que le asignamos a la
variable "nombre", es decir, lo guardamos en una posición de memoria cualquiera, que
var
dato: record
nombre: string[20];
edad: byte;
end;
begin
[Link]:='José Ignacio';
[Link]:=23;
write('El nombre es ', [Link] );
write(' y la edad ', [Link], ' años.');
end.
program Ejemplo_de_registro;
var
dato: record
nombre: string[20];
edad: byte;
end;
begin
with dato do
begin
nombre:='José Ignacio';
edad:=23;
write('El nombre es ',nombre);
write(' y la edad ',edad,' años.');
end;
end.
program NuevoValor;
var
numero: integer;
begin
numero := 25;
writeln('La variable vale ', numero);
numero := 50;
writeln('Ahora vale ', numero);
numero := numero + 10;
writeln('Y ahora ', numero);
writeln('Introduce ahora tú el valor');
readln( numero );
writeln('Finalmente, ahora vale ', numero);
end.
var
numero1, numero2, suma: integer;
begin
writeln('Introduce el primer número');
readln( numero1 );
writeln('Introduce el segundo número');
readln( numero2 );
suma := numero1 + numero2;
writeln('La suma de los dos números es: ', suma);
end.
Ejemplo 3: (Media de los elementos de un vector)
• Este es un programa nada optimizado, para que se adapte
a los conocimientos que tenemos por ahora y se vea cómo
se manejan los Arrays. Admite muchas mejoras, que iremos
viendo más adelante.
• Como novedades sobre la lección, incluye la forma de dejar
una línea de pantalla en blanco (con writeln), o de definir de
una sola vez varias variables que sean del mismo tipo,
separadas por comas. Las operaciones matemáticas se
verán con más detalle más adelante.
program MediadelVector;
var
vector: array [1..5] of real;
suma, media: real;
begin
writeln('Media de un vector con 5 elementos.');
writeln;
writeln('Introduce el primer elemento');
readln(vector[1]);
writeln('Introduce el segundo elemento');
readln(vector[2]);
writeln('Introduce el tercer elemento');
readln(vector[3]);
writeln('Introduce el cuarto elemento');
readln(vector[4]);
writeln('Introduce el quinto elemento');
readln(vector[5]);
suma := vector[1] + vector[2] + vector[3] +
vector[4] + vector[5];
media := suma / 5;
writeln('La media de sus elementos es: ', media);
end.
Ya hemos visto por encima las dos formas más habituales de mostrar datos en
pantalla, con "write" o "writeln", y de aceptar la introducción de datos por parte del usuario,
con "readln" (o "read", que no efectúa un retorno de carro después de leer los datos).
Veamos ahora su manejo y algunas de sus posibilidades con más detalle:
Cuando se desee escribir varias cosas en la misma línea, todas ellas se indican
entre un mismo paréntesis, y separadas por comas. Se puede especificar la anchura de lo
escrito, mediante el símbolo de dos puntos (:) y la cifra que indique la anchura. Si se trata
de un número real y queremos indicar también el número de decimales, esto se hace
también después de los dos puntos, con el formato ":anchura_total:decimales". Como
ejemplos:
Igual ocurre con los números: si es más grande que la anchura indicada, no se
"parte", sino que se escribe completo. Si es menor, se rellena con espacios por la
izquierda. Los decimales sí que se redondean al número de posiciones indicado:
1.2345678900E+06
1234567.890
.ej1234567.89
.ej1234567.9
.ej1234568
1.2E+06
para que nos resulte más legible o para aclarar lo que hace una línea o un conjunto de
líneas.
En Pascal, los comentarios se encierran entre (* y *). También está permitido usar { y },
tanto en Turbo Pascal como en SURPAS. Como se ve en el ejemplo, pueden ocupar más
de una línea.
Para tomar datos del usuario, la forma más directa es empleando readln, que toma un
texto o un número y asigna este valor a una variable. No avisa de lo que está haciendo,
así que normalmente convendrá escribir antes en pantalla un mensaje que indique al
usuario qué esperamos que teclee:
• La edición es incómoda: para corregir un error sólo podemos borrar todo lo que
habíamos escrito desde entonces, no podemos usar las flechas o INICIO/FIN para
desplazarnos por el texto.
• Si queremos dar un valor a una variable numérica y pulsamos " 23" (un espacio
delante del número) le dará un valor 0.
Más adelante, veremos que existen formas mucho más versátiles y cómodas de leer
datos a través del teclado, en el mismo tema en el que veamos cómo se maneja la
pantalla en modo texto desde Pascal...
En Pascal contamos con una serie de operadores para realizar sumas, restas,
multiplicaciones y otras operaciones no tan habituales:
enteros entero
+ Suma
reales real
enteros entero
- Resta
reales real
enteros entero
* Multiplicación
reales real
enteros real
/ División
reales real
program operaciones;
var
e1, e2: integer; (* Números enteros *)
r1, r2, r3: real; (* Números reales *)
begin
e1:=17;
e2:=5;
r1:=1;
r2:=3.2;
writeln('Empezamos...');
r3:=r1+r2;
writeln('La suma de r1 y r2 es :', r3);
writeln(' o también ', r1+r2 :5:2); (* Indicando el formato *)
writeln('El producto de r1 y r2 es :', r1 * r2);
writeln('El valor de r1 dividido entre r2 es :', r1 / r2);
writeln('La diferencia de e2 y e1 es : ', e2 - e1);
writeln('La división de e1 entre e2 : ', e1 / e2);
writeln(' Su división entera : ', e1 div e2);
writeln(' Y el resto de la división : ', e1 mod e2);
writeln('El opuesto de e2 es :', -e2);
end.
El operador + (suma) se puede utilizar también para concatenar cadenas de texto, así:
var
texto1, texto2, texto3: string;
begin
texto1 := 'Hola ';
texto2 := '¿Cómo estás?';
texto3 := texto1 + texto2;
writeln(texto3); (* Escribirá "Hola ¿Cómo estás?" *)
end.
Operadores lógicos
Vimos de pasada que en el tema que había unos tipos de datos llamados "boolean",
y que podían valer TRUE (verdadero) o FALSE (falso). En la próxima lección veremos
cómo hacer comparaciones del estilo de "si A es mayor que B y B es mayor que C", y
empezaremos a utilizar variables de este tipo, pero vamos a mencionar ya eso del "y".
Podremos encadenar proposiciones de ese tipo (si A y B entonces C) con: and (y),
or (ó), not (no) y los operadores relacionales, que se usan para comparar y son los
siguientes:
Operador Operación
= Igual a
<> No igual a (distinto de)
< Menor que
> Mayor que
<= Menor o igual que
>= Mayor o igual que
Igual que antes, algunos de ellos (>=, <=, in) los utilizaremos también en los
conjuntos, más adelante.
Los operadores "and", "or" y "not", junto con otros, se pueden utilizar también para
operaciones entre bits de números enteros.
Operador Operación
not Negación
or Suma lógica
begin
writeln('Allá vamos... ');
writeln( 5+3+4*5*2 );
writeln( (5+3)*4+3*5-8/2+7/(3-2) );
writeln( 5 div 3 + 23 mod 4 - 4 * 5 );
writeln( 125 and 6 ); (* Este para los más osados *)
end.
3.5 Condiciones
program if1;
begin
writeln('Escriba un número');
readln(numero);
if numero>0 then writeln('El número es positivo');
end.
La "condición" debe ser una expresión que devuelva un valor del tipo "boolean"
(verdadero/falso). La sentencia se ejecutará si ese valor es "cierto" (TRUE). Este valor
puede ser tanto el resultado de una comparación como la anterior, como una propia
variable booleana. Así, una forma más "rebuscada" (pero que a veces resultará más
cómoda y más legible) de hacer lo anterior sería:
program if2;
var
numero: integer;
esPositivo: boolean;
begin
writeln('Escriba un número');
readln(numero);
esPositivo := (numero>0);
if esPositivo then writeln('El número es positivo');
end.
Cuando veamos más adelante las órdenes para controlar el flujo del programa,
seguiremos descubriendo aplicaciones de las variables booleanas, que muchas veces uno
considera "poco útiles" cuando está aprendiendo.
La "sentencia" puede ser una sentencia simple o una compuesta. Las sentencias
compuestas se forman agrupando varias simples entre un "begin" y un "end":
program if3;
var
numero: integer;
begin
writeln('Escriba un número');
readln(numero);
if numero<0 then
begin
writeln('El número es negativo. Pulse INTRO para seguir.');
readln
end;
end.
program if4;
var
numero: integer;
begin
writeln('Escriba un número');
readln(numero);
if numero<0 then
writeln('El número es negativo.')
else
writeln('El número es positivo o cero.')
end.
Un detalle importante que conviene tener en cuenta es que antes del "else" no
debe haber un punto y coma, porque eso indicaría el final de la sentencia "if...", y el
compilador nos avisaría con un error.
program if5;
var
numero: integer;
begin
writeln('Escriba un número');
readln(numero);
if numero<0 then
writeln('El número es negativo.')
else if numero>0 then
writeln('El número es positivo.')
else
writeln('El número es cero.')
end.
Si se deben cumplir varias condiciones a la vez, podemos enlazarlas con "and" (y).
Si se pueden cumplir varias, usaremos "or" (o). Para negar, "not" (no):
Pero cuando queremos comprobar entre varios posibles valores, sería muy pesado
tener que hacerlo con muchos "if" seguidos o encadenar muchos con "and" u "or". Hay
una alternativa que resulta mucho más cómoda: la orden case. Su sintaxis es
case expresión of
caso1: sentencia1;
caso2: sentencia2;
...
casoN: sentenciaN;
end;
O bien, si queremos indicar lo que se debe hacer si no coincide con ninguno de los
valores que hemos enumerado, usamos else:
case expresión of
caso1: sentencia1;
caso2: sentencia2;
...
casoN: sentenciaN;
else
otraSentencia;
end;
program case1;
var
letra: char;
begin
WriteLn('Escriba un letra');
ReadLn(letra);
case letra of
' ': WriteLn('Un espacio');
'A'..'Z', 'a'..'z': WriteLn('Una letra');
'0'..'9': WriteLn('Un dígito');
'+', '-', '*', '/': WriteLn('Un operador');
else
WriteLn('No es espacio, ni letra, ni dígito, ni operador');
end;
end.
Y como se ve en el ejemplo, los "casos" posibles pueden ser valores únicos, varios
valores separados por comas, o un rango de valores separados por .. (como los puntos
suspensivos, pero sólo dos).
3.6 Bucles
Vamos a ver cómo podemos crear bucles, es decir, partes del programa que se
repitan un cierto número de veces.
Según cómo queramos que se controle ese bucle, tenemos tres posibilidades, que
vamos a empezar a ver ya por encima:
• for..to: La orden se repite desde que una variable tiene un valor inicial hasta que
alcanza otro valor final (un cierto NUMERO de veces).
• while..do: Repite una sentencia MIENTRAS que sea cierta la condición que
indicamos.
• repeat..until: Repite un grupo de sentencias HASTA que se dé una condición.
La diferencia entre estos dos últimos es que "while" comprueba la condición antes de
ejecutar las otras sentencias, por lo que puede que estas sentencias ni siquiera se lleguen
a ejecutar, si la condición de entrada es falsa. En "repeat", la condición se comprueba al
final, de modo que las sentencias intermedias se ejecutarán al menos una vez.
• For
var
contador: integer;
begin
for contador := 1 to 10 do
writeln( contador );
end.
Los bucles "for" se pueden enlazar uno dentro de otro, de modo que un segundo
ejemplo que escribiera las tablas de multiplicar del 1 al 5 podría ser
var
tabla, numero: integer;
begin
for tabla := 1 to 5 do
for numero := 1 to 10 do
writeln( tabla, 'por ', numero ,'es', tabla * numero );
end.
Hasta ahora hemos visto sólo casos en los que después de "for" había una única
sentencia. ¿Qué ocurre si queremos repetir más de una orden? Basta encerrarlas entre
"begin" y "end" para convertirlas en una sentencia compuesta.
Así, vamos a mejorar el ejemplo anterior haciendo que deje una línea en blanco
entre tabla y tabla:
var
tabla, numero: integer;
begin
for tabla := 1 to 5 do
begin
for numero := 1 to 10 do
writeln( tabla, 'por ', numero ,'es', tabla * numero );
writeln; (* Línea en blanco *)
end;
end.
var
letra: char;
begin
for letra := 'a' to 'z' do
write( letra );
end.
Como último comentario: con el bucle "for", tal y como lo hemos visto, sólo se
puede contar en forma creciente y de uno en uno. Para contar de forma decreciente, se
usa "downto" en vez de "to".
Para contar de dos en dos (por ejemplo), hay usar "trucos": multiplicar por dos o
sumar uno dentro del cuerpo del bucle, etc... Eso sí, sin modificar la variable que controla
el bucle (usar cosas como "write(x*2)" en vez de "x := x*2", que pueden dar problemas en
algunos compiladores).
• While
La sintaxis de "while"
while condición do
sentencia;
Un ejemplo que nos diga la longitud de todas las frases que queramos es:
var
frase: string;
begin
writeln('Escribe frases, y deja una línea en blanco para salir');
write( '¿Primera frase?' );
readln( frase );
while frase <> '' do
begin
writeln( 'Su longitud es ', length(frase) );
write( '¿Siguiente frase?' );
readln( frase )
end
end.
Como comentario casi innecesario, length es una función que nos dice cuantos
caracteres componen una cadena de texto.
while (2<1) do
writeln('Dos es menor que uno');
• Repeat .. Until.
repeat
sentencia;
...
sentencia;
sentencia
until condición;
Como último detalle, de menor importancia, no hace falta terminar con punto y
coma la sentencia que va justo antes de "until", al igual que ocurre con "end".
program ClaveDeAcceso;
var
ClaveCorrecta, Intento: String;
begin
ClaveCorrecta := 'PascalForever';
repeat
WriteLn( 'Introduce la clave de acceso...' );
ReadLn( Intento )
until Intento = ClaveCorrecta
(* Aquí iría el resto del programa *)
end.
Por cierto, si alguien ha programado en Basic puede que se pregunte por la orden
goto. Existe en Pascal, pero su uso va a en contra de todos los principios de la
Programación Estructurada, solo está "medio-permitido" en casos muy concretos, así que
lo veremos más adelante.
• Ejercicios resueltos:
Antes que nada, hay que puntualizar una cosa: el que así se resuelva de una forma
no quiere decir que no se pueda hacer de otras. Ni siquiera que la mía sea la mejor,
porque trataré de adaptarlos al nivel que se supone que tenemos.
program del2al16;
var i: integer;
begin
for i := 1 to 8 do
writeln( i*2 );
end.
program del6al1;
var i: integer;
begin
for i := 6 downto 1 do
writeln( i );
end.
program del3al21;
var i: integer;
begin
for i := 1 to 10 do
writeln( i*2 +1 );
end.
program del12al0;
var i: integer;
begin
for i := 6 downto 0 do
writeln( i*2 );
end.
5.- Otro que multiplique dos matrices.
Saltamos la parte que declara las variables y que pide los datos. La parte de la
multiplicación sería, para matrices cuadradas de 10 por 10, por ejemplo:
for i := 1 to 10 do
for j := 1 to 10 do
c[i,j] := 0; (* limpia la matriz destino *)
for i :=1 to 10 do
for j := 1 to 10 do
for k := 1 to 10 do
c[i,j] := c[i,j] + a[k,j] * b[i,k];
7.- Mejorar el programa de la clave de acceso para que avise de que la clave no es
correcta.
program ClaveDeAcceso2;
var
ClaveCorrecta, Intento: String;
begin
ClaveCorrecta := 'PascalForever';
repeat
WriteLn( 'Introduce la clave de acceso...' );
ReadLn( Intento )
if Intento <> ClaveCorrecta then
writeln( ' Esa no es la clave correcta! ');
until Intento = ClaveCorrecta
(* Aquí iría el resto del programa *)
end.
8.- Mejorar más todavía para que sólo haya tres intentos.
program ClaveDeAcceso3;
var
ClaveCorrecta, Intento: String;
NumIntento: integer; (* número de intento *)
begin
ClaveCorrecta := 'PascalForever';
NumIntento := 0; (* aún no hemos probado *)
repeat
NumIntento := NumIntento + 1; (* siguiente intento *)
WriteLn( 'Introduce la clave de acceso...' );
ReadLn( Intento )
if Intento <> ClaveCorrecta then
begin
writeln( ' Esa no es la clave correcta! ');
if NumIntentos = 3 then exit (* sale si es el 3º *)
end
until Intento = ClaveCorrecta
(* Aquí iría el resto del programa *)
end.
9.- Adaptar la primera versión (el ejemplo), la segunda (la mejorada) y la tercera (re-
mejorada) para que empleen "while" y no "repeat-until"
Sólo ponemos una de ellas, porque las demás son muy similares.
var
ClaveCorrecta, Intento: String;
begin
ClaveCorrecta := 'PascalForever';
Intento := ''; (* cadena vacía *)
while Intento <> ClaveCorrecta do (* mientras no acertemos *)
begin
WriteLn( 'Introduce la clave de acceso...' );
ReadLn( Intento )
if Intento <> ClaveCorrecta then
writeln( ' Esa no es la clave correcta! ');
end; (* fin del "while" *)
(* Aquí iría el resto del programa *)
end.
• Definición de constantes.
Cuando desarrollamos un programa, nos podemos encontrar con que hay variables
que realmente "no varían" a lo largo de la ejecución de un programa, sino que su valor es
constante.
Hay una manera especial de definirlas, que es con el especificador "const", que tiene el
formato:
Estas constantes se manejan igual que variables como las que habíamos visto
hasta hora, sólo que no se puede cambiar su valor. Así, es valido hacer
Writeln(MiNombre);
if Longitud > LongitudMaxima then ...
OtraVariable := MiNombre;
LongCircunf := 2 * PI * r;
PI := 3.14;
MiNombre := 'Luis';
LongitudMaxima := LongitudMaxima + 10;
Las constantes son mucho más prácticas de lo que puede parecer a primera vista
(especialmente para quien venga de lenguajes como Basic, en el que no existen -en el
Basic "de siempre", puede que sí exista en los últimas versiones del lenguaje). Me explico
con un ejemplo:
Supongamos que estamos haciendo nuestra agenda en Pascal (ya falta menos
para que sea verdad), y estamos tan orgullosos de ella que queremos que en cada
pantalla de cada parte del programa aparezca nuestro nombre, el del programa y la
versión actual. Si lo escribimos de nuevas cada vez, además de perder tiempo tecleando
más, corremos el riesgo de que un día queramos cambiar el nombre (ya no se llamará
"Agenda" sino "SuperAgenda") pero lo hagamos en unas partes sí y en otras no, etc., y el
resultado tan maravilloso quede estropeado por esos "detalles".
La solución será definir todo ese tipo de datos como constantes al principio del
programa, de modo que con un vistazo a esta zona podemos hacer cambios globales:
const
Nombre = 'Luis';
Prog = 'SuperAgenda en Pascal';
Versión = 1.95;
LongNombre = 40;
LongTelef = 9;
LongDirec = 60;
...
Las declaraciones de las constantes se hacen antes del cuerpo del programa
principal, y generalmente antes de las declaraciones de variables:
program MiniAgenda;
const
NumFichas = 50;
var
Datos: array[ 1..NumFichas ] of string;
begin
...
El identificador "const" tiene también en Turbo Pascal otro uso menos habitual:
definir lo que se llaman constantes con tipo, que son variables normales y corrientes,
pero a las que damos un valor inicial antes de que comience a ejecutarse el programa. Se
usa
Así, volviendo al ejemplo de la clave de acceso, podíamos tener una variables "intentos"
que dijese el número de intentos. Hasta ahora habríamos hecho
var
intentos: integer;
begin
intentos := 3;
...
Ahora ya sabemos que sería mejor hacer, si sabemos que el valor no va a cambiar:
const
intentos = 3;
begin
...
Pero si se nos da el caso de que vemos por el nombre que es alguien de confianza,
que puede haber olvidado su clave de acceso, quizá nos interese permitirle 5 o más
intentos. Ya no podemos usar "const" porque el valor puede variar, pero por otra parte,
siempre comenzamos concediendo 3 intentos, hasta comprobar si es alguien de fiar.
Podemos hacer
begin
...
Recordemos que una "constante con tipo" se manejará exactamente igual que
una variable, con las ventajas de que está más fácil de localizar si queremos cambiar su
valor inicial y de que el compilador optimiza un poco el código, haciendo el programa unos
bytes más pequeño.
• Definición de tipos.
indica que vamos a usar una variable que se va a llamar PrimerNumero y que almacenará
valores de tipo entero. Si queremos definir una de las fichas de lo que será nuestra
agenda, también haríamos:
Tampoco hay ningún problema con esto, ¿verdad? Y si podemos utilizar variables
creando los tipos "en el momento", como en el caso anterior, ¿para qué necesitamos
definir tipos? Vamos a verlo con un ejemplo. Supongamos que vamos a tener ahora dos
variables: una "ficha1" que contendrá el dato de la ficha actual y otra "ficha2" en la que
almacenaremos datos temporales. Veamos qué pasa...
program PruebaTipos;
var
ficha1: record
nombre: string;
direccion: string;
edad: integer;
observaciones: string
end;
ficha2: record
nombre: string;
direccion: string;
edad: integer;
observaciones: string
end;
begin
[Link] := 'Pepe';
[Link] := 'Su casa';
[Link] := 65;
[Link] := 'El mayor de mis amigos...';
ficha2 := ficha1;
writeln( [Link]);
end.
Veamos qué haría este programa: define dos variables que van a guardar la misma
clase de datos. Da valores a cada uno de los datos que almacenará una de ellas. Después
hacemos que la segunda valga lo mismo que la primera, e imprimimos el nombre de la
segunda. Aparecerá escrito "Pepe" en la pantalla...
No. Aunque a nuestros ojos "ficha1" y "ficha2" sean iguales, para el compilador no
es así, por lo que protesta y el programa ni siquiera llega a ejecutarse. Es decir: las hemos
definido para que almacene la misma clase de valores, pero no son del mismo tipo.
begin
...
Si las definimos a la vez, SI QUE SON DEL MISMO TIPO. Pero surge un problema
que se entenderá mejor más adelante, cuando empecemos a crear funciones y
procedimientos. ¿Qué ocurre si queremos usar en alguna parte del programa otras
variables que también sean de ese tipo? ¿Las definimos también a la vez? En muchas
ocasiones no será posible.
Así que tiene que haber una forma de indicar que todo eso que sigue a la palabra
"record" es un tipo al que nosotros queremos acceder con la misma comodidad que si
fuese "integer" o "boolean", queremos definir un tipo, no simplemente declararlo, como
estábamos haciendo.
Pues es sencillo:
o en nuestro caso
Ahora sí que podremos asignar valores entre variables que hayamos definido en
distintas partes del programa, podremos usar esos tipos para crear ficheros (que también
veremos más adelante), etc.
• Conceptos básicos.
begin
InicializaVariables;
PantallaPresentacion;
Repeat
PideOpcion;
case Opcion of
'1': MasDatos;
'2': CorregirActual;
'3': Imprimir;
...
end;
Until Opcion = OpcionDeSalida;
GuardaCambios;
LiberaMemoria
end.
La diferencia entre ellos es que un procedimiento ejecuta una serie de acciones que
están relacionadas entre sí, y no devuelve ningún valor, mientras que la función sí que va
a devolver valores. Veámoslo con un par de ejemplos:
procedure Acceso;
var
clave: string; (* Esta variable es local *)
begin
writeln(' Bienvenido a SuperAgenda ');
writeln('=========================='); (* Para subrayar *)
writeln; writeln; (* Dos líneas en blanco *)
writeln('Introduzca su clave de acceso');
readln( clave ); (* Lee un valor *)
if clave <> ClaveCorrecta then (* Compara con el correcto *)
begin (* Si no lo es *)
writeln('La clave no es correcta!'); (* avisa y *)
exit (* abandona el programa *)
end
end;
Veamos el segundo ejemplo: una función que eleve un número a otro (no existe en
Pascal), se podría hacer así, si ambos son enteros:
var
numero1, numero2: integer; (* Variable globales *)
Un procedimiento también puede tener "parámetros", igual que la función que acabamos
de ver:
program ProcConParametros;
En el próximo apartado veremos la diferencia entre pasar parámetros por valor (lo
que hemos estado haciendo) y por referencia (para poder modificarlos), y jugaremos un
poco con la recursividad.
• Adaptar la función "potencia" que hemos visto para que trabaje con números reales,
y permita cosas como 3.2 ^ 1.7
• Hacer una función que halle la raíz cúbica del número que se le indique.
• Definir las funciones suma y producto de tres números y hacer un programa que
haga una operación u otra según le indiquemos (con "case", etc).
• Parámetros.
Ya habíamos visto, sin entrar en detalles, qué es eso de los parámetros: una serie de
datos extra que indicábamos entre paréntesis en la cabecera de un procedimiento o
función.
Es algo que estamos usando, sin saberlo, desde el primer tema, cuando empezamos a
usar "WriteLn":
writeln( 'Hola' );
program PruebaDeParametros;
begin
dato := 2;
writeln( dato );
modifica( dato );
writeln( dato );
end.
No: Escribe un 2. Las modificaciones que hagamos a "dato" dentro del procedimiento
modifica sólo son válidas mientras estemos dentro de ese procedimiento. Lo que
modificamos es la variable genérica que hemos llamado "variable", y que no existe fuera
del procedimiento.
program PruebaDeParametros2;
begin
dato := 2;
writeln( dato );
modifica( dato );
writeln( dato );
end.
Esta vez la última línea del programa sí que escribe un 3 y no un 2, porque hemos
permitido que los cambios hechos a la variable salgan del procedimiento. Esto es pasar
un parámetro por referencia.
Una de las aplicaciones más habituales de pasar parámetros por referencia es cuando una
función debe devolver más de un valor. Habíamos visto que una función era como un
procedimiento, pero además devolvía un valor (pero sólo uno). Si queremos obtener más
de un valor de salida, una de las formas de hacerlo es pasándolos como parámetros,
precedidos por la palabra "var".
Y como ejercicio queda un caso un poco más "enrevesado". Qué ocurre si el primer
programa lo modificamos para que sea así:
program PruebaDeParametros3;
begin
dato := 2;
writeln( dato );
modifica( dato );
writeln( dato );
end.
• Recursividad.
Luego podemos escribir cada factorial en función del factorial del siguiente número:
n! = n · (n-1)!
Acabamos de dar la definición recursiva del factorial. Ahora sólo queda ver cómo se haría
eso programando:
program PruebaDeFactorial;
begin
writeln( 'Introduce un número entero (no muy grande) ' );
readln(numero);
writeln( 'Su factorial es ', factorial(numero) );
end.
Un par de ejercicios:
• Soluciones.
Aquí van unas soluciones (insisto en que no tienen por qué ser las únicas ni las mejores) a
los ejercicios propuestos:
1.- Adaptar la función "potencia" que hemos visto para que trabaje con números
reales, y permita cosas como 3.2 ^ 1.7
No basta con cambiar los tipos de las variables, poniendo "real" en vez de "integer". Si
hacemos esto, ni siquiera podremos compilar el programa, porque una variable de tipo
"real" no se puede usar para controlar un bucle.
2.- Hacer una función que halle la raíz cúbica del número que se le indique.
Mirando la función anterior, ya es fácil: una raíz cúbica es lo mismo que elevar a 1/3, así
que se puede hacer:
3.- Definir las funciones suma y producto de tres números y hacer un programa que
haga una operación u otra según le indiquemos (con "case", etc.).
program Suma_Y_Producto;
var
num1, num2: real; (* Definición de variables *)
opcion: char;
begin
write( 'Introduzca el primer número: ');
readln( num1 );
write( 'Introduzca el segundo número: ');
readln( num2 );
writeln( '¿Qué operación desea realizar?' );
writeln( 's = Suma p = Producto' );
readln( opcion );
case opcion of
's': writeln( 'Su suma es ', suma(num1,num2) );
'p': writeln( 'Su producto es ', producto(num1,num2) );
else
writeln( 'Operación desconocida!' );
end; (* Fin del case *)
end. (* y del programa *)
4.- Hallar una función recursiva que halle el producto de dos números enteros
positivos.
Este es un caso con poco sentido en la práctica, pero que se puede resolver simplemente
recordando que un producto no son más que varias sumas:
begin
write( 'Introduzca el primer número: ');
readln( num1 );
write( 'Introduzca el segundo número: ');
readln( num2 );
writeln( 'Su producto es ', producto(num1,num2) );
end.
La idea es la misma que antes: simplificar el problema, esta vez escribiendo la potencia
como varios productos:
Comenzamos a ver los tipos de datos que podíamos manejar en el tema 2. En aquel
momento tratamos los siguientes:
Esta vez vamos a ampliar con otros tipos de datos que podemos encontrar en Turbo
Pascal (aunque puede que no en otras versiones del lenguaje Pascal).
• Enteros.
• Correspondencia byte-char.
• Reales del 8087.
• Tipos enumerados.
• Más detalles sobre Strings.
• Registros variantes.
• Conjuntos.
Vamos allá:
Estos tipos, junto con "char" (y "boolean" y otros para los que no vamos a entrar en tanto
detalle) son tipos ordinales, existe una relación de orden entre ellos y cada elemento está
precedido y seguido por otro que podemos conocer (cosa que no ocurre en los reales).
Para ellos están definidas las funciones:
El uso más habitual de "ord" es para convertir de "char" a "byte". Los caracteres se
almacenan en memoria, de tal forma que a cada uno se le asigna un número entre 0 y
255, su "código ASCII" (ej: A=65, a=96, 0=48, Ó=224). La forma de hallar el código ASCII
de una letra es simplemente ord(letra), como ord('Ó').
El paso contrario, la letra que corresponde a cierto número, se hace con la función "chr".
Así, podemos escribir los caracteres "imprimibles" de nuestro ordenador sabiendo que van
del 32 al 255 (los que están por debajo de 32 suelen ser caracteres de control, que en
muchos casos no se podrán mostrar en pantalla):
begin
for bucle := 32 to 255 do
write( chr(bucle) );
end.
Declaramos las variables igual que hacíamos con cualquier otro tipo:
begin
dia := Lunes;
[...]
if dia = Viernes then
writeln( 'Se acerca el fin de semana!' );
[...]
Los tipos enumerados también son tipos ordinales, por lo que podemos usar pred, succ y
ord con ellos. Así, el en ejemplo anterior
Una última curiosidad: habíamos dicho que se guarda también la longitud de la cadena.
Esto se hace en la posición 0. Veamos un programa de ejemplo:
var
linea: string [20]; (* Cadena inicial: limitada a 20 letras *)
pos: byte; (* Posición que estamos mirando *)
begin
writeln( 'Introduce una línea de texto...' );
readln( linea );
for pos := 1 to ord(linea[0]) do
writeln(' La letra número ', pos,' es una ', linea[pos]);
end.
Comentarios:
También habíamos visto ya los registros (records), pero con unos campos fijos. No tiene
por qué ser necesariamente así. Tenemos a nuestra disposición los registros variantes,
en los que con un "case" podemos elegir unos campos u otros. La mejor forma de
entenderlos es con un ejemplo.
program RegistrosVariantes;
type
TipoDato = (Num, Fech, Str);
Fecha = record
D, M, A: Byte;
end;
Ficha = record
Nombre: string[20]; (* Campo fijo *)
case Tipo: TipoDato of (* Campos variantes *)
Num: (N: real); (* Si es un número: campo N *)
Fech: (F: Fecha); (* Si es fecha: campo F *)
Str: (S: string); (* Si es string: campo S *)
end;
var
UnDato: Ficha;
begin
[Link] := 'Luis'; (* Campo normal de un record *)
[Link] := Num; (* Vamos a almacenar un número *)
UnDato.N := 3.5; (* que vale 3.5 *)
Finalmente, tenemos los conjuntos (sets). Un conjunto está formado por una serie de
elementos de un tipo base, que debe ser un ordinal de no más de 256 valores posibles,
como un "char", un "byte" o un enumerado.
type
Letras = set of Char;
Para construir un "set" utilizaremos los corchetes ([ ]), y dentro de ellos enumeramos los
valores posibles, uno a uno o como rangos de valores separados por ".." :
var
LetrasValidas : Letras;
Fiesta : Dias;
begin
LetrasValidas = ['a'..'z', 'A'..'z', '0'..'9', 'ñ', 'Ñ']
Fiesta = [ Sabado, Domingo ]
end.
Un conjunto vacío se define con [ ]. Las operaciones que tenemos definidas sobre los
conjuntos son:
Operac ¦ Nombre
---------+----------------
+ ¦ Unión
- ¦ Diferencia
* ¦ Intersección
in ¦ Pertenencia
En el primer ejemplo hemos dicho que el conjunto de vocales permitidas (que deberíamos
haber declarado) es la intersección de las vocales (que también debíamos haber
declarado) y las letras válidas.
Este tema va a ser específico de Turbo Pascal para DOS. Algunas de las cosas que
veremos aparecen en otras versiones de Turbo Pascal (la 3.0 para CP/M, por ejemplo),
pero no todas, o en otros compiladores (como TMT Pascal Lite) y de cualquier modo, nada
de esto es Pascal estándar.
Nos centraremos primero en cómo se haría con las versiones 5.0 y superiores de Turbo
Pascal. Luego comentaremos cómo se haría con Turbo Pascal 3.01.
Veremos cómo crearlas un poco más adelante, pero de momento nos va a interesar saber
cómo acceder a ellas, porque Turbo Pascal incorpora unidades que aportan mayores
posibilidades de manejo de la pantalla en modo texto o gráfico, de acceso a funciones del
DOS, de manejo de la impresora, etc.
Así, la primera unidad que trataremos es la encargada de gestionar (entre otras cosas) la
pantalla en modo texto. Esta unidad se llama CRT. Para acceder a cualquier unidad, se
program prueba;
uses crt;
var
[...]
Vamos a mencionar algunos de los procedimientos y funciones más importantes. Para ver
los demás, basta pasearse por la ayuda de Turbo Pascal: escribir crt y pulsar Ctrl+F1
encima de esa palabra, que nos muestra información sobre esa palabra clave (o la que
nos interesase).
Comentarios generales:
• X es la columna, de 1 a 80.
• Y es la fila, de 1 a 25.
program PruebaDeCRT;
uses crt;
var
bucle : byte;
tecla : char;
begin
ClrScr; { Borra la pantalla }
TextColor( Yellow ); { Color amarillo }
TextBackground( Red ); { Fondo rojo }
GotoXY( 40, 13 ); { Vamos al centro de la pantalla }
Write(' Hola '); { Saludamos }
Delay( 1000 ); { Esperamos un segundo }
Window ( 1, 15, 80, 23 ); { Ventana entre las filas 15 y 23 }
TextBackground ( Blue ); { Con fondo azul }
ClrScr; { La borramos para que se vea }
for bucle := 1 to 100
do WriteLn( bucle ); { Escribimos del 1 al 100 }
WriteLn( 'Pulse una tecla..');
tecla := ReadKey; { Esperamos que se pulse una tecla }
Window( 1, 1, 80, 25 ); { Restauramos ventana original }
GotoXY( 1, 24 ); { Vamos a la penúltima línea }
Write( 'Ha pulsado ', tecla ); { Pos eso }
Sound( 220 ); { Sonido de frecuencia 220 Hz }
Delay( 500 ); { Durante medio segundo }
NoSound; { Se acabó el sonido }
Delay( 2000 ); { Pausa antes de acabar }
TextColor( LightGray ); { Colores por defecto del DOS }
TextBackground( Black ); { Y borramos la pantalla }
ClrScr;
end.
Ahora vamos a ver cómo podemos leer ficheros y crear los nuestros propios. Voy a dividir
este tema en tres partes: primero veremos como manejar los ficheros de texto, luego los
ficheros "con tipo" (que usaremos para hacer una pequeña agenda), y finalmente los
ficheros "generales".
Las diferencias entre las dos últimas clases de fichero pueden parecer poco claras ahora e
incluso en el próximo apartado, pero en cuanto hayamos visto todos los tipos de ficheros
se comprenderá bien cuando usar unos y otros.
Vayamos a lo que nos interesa hoy: un fichero de texto. Es un fichero formado por
caracteres ASCII normales, que dan lugar a líneas de texto legible.
Casi salta a la vista que los ficheros del primer tipo, los de texto, van a ser más fáciles de
tratar que los "ficheros en general". Hasta cierto punto es así, y por eso es por lo que
vamos a empezar por ellos.
program MuestraAutoexec;
var
fichero: text; (* Fichero de texto *)
linea: string; (* Línea que leemos *)
begin
assign( fichero, 'C:\[Link]' ); (* Le asignamos el nombre *)
reset( fichero ); (* Lo abrimos para lectura *)
while not eof( fichero ) do (* Mientras que no se acabe *)
begin
readln( fichero, linea ); (* Leemos una línea *)
writeln( linea ); (* y la mostramos *)
end;
close( fichero ); (* Se acabó: lo cerramos *)
end.
Eso es todo. Debería ser bastante autoexplicativo, pero aun así vamos a comentar
algunas cosas:
Este fichero está abierto para lectura. Si queremos abrirlo para escritura, empleamos
"rewrite" en vez de "reset", pero esta orden hay que utilizarla con cuidado, porque si el
fichero ya existe lo machacaría, dejando el nuevo en su lugar, y perdiendo los datos
anteriores. Más adelante veremos cómo comprobar si el fichero ya existe.
Para abrir el fichero para añadir texto al final, usaríamos "append" en vez de "reset". En
ambos casos, los datos se escribirían con
Una limitación que puede parecer importante es eso de que el fichero debe existir, y si no
el programa se interrumpe. En la práctica, esto no es tan drástico. Hay una forma de
comprobarlo, que es con una de las llamadas "directivas de compilación". Obligamos al
compilador a que temporalmente no compruebe las entradas y salidas, y lo hacemos
nosotros mismos. Después volvemos a habilitar las comprobaciones. Ahí va un ejemplo de
cómo se hace esto:
program MuestraAutoexec2;
var
fichero: text; (* Fichero de texto *)
linea: string; (* Línea que leemos *)
begin
assign( fichero, 'C:\[Link]' ); (* Le asignamos el nombre *)
{$I-} (* Deshabilita comprobación
de entrada/salida *)
reset( fichero ); (* Lo intentamos abrir *)
{$I+} (* La habilitamos otra vez *)
if ioResult = 0 then (* Si todo ha ido bien *)
begin
while not eof( fichero ) do (* Mientras que no se acabe *)
begin
readln( fichero, linea ); (* Leemos una línea *)
writeln( linea ); (* y la mostramos *)
end;
close( fichero ); (* Se acabó: lo cerramos *)
end; (* Final del "if" *)
end.
Como último ejemplo sobre este tema, un programa que lea el [Link] y cree
una copia en el directorio actual llamada [Link]. La orden del DOS equivalente
sería COPY C:\[Link] [Link]
program CopiaAutoexec;
var
fichero1, fichero2: text; (* Ficheros de texto *)
linea: string; (* Línea actual *)
begin
assign( fichero1, 'C:\[Link]' ); (* Le asignamos nombre *)
assign( fichero2, '[Link]' ); (* y al otro *)
{$I-} (* Sin comprobación E/S *)
reset( fichero1 ); (* Intentamos abrir uno *)
{$I+} (* La habilitamos otra vez *)
if ioResult = 0 then (* Si todo ha ido bien *)
begin
rewrite( fichero2 ); (* Abrimos el otro *)
while not eof( fichero1 ) do (* Mientras que no acabe 1 *)
begin
readln( fichero1, linea ); (* Leemos una línea *)
writeln( fichero2, linea ); (* y la escribimos *)
end;
writeln( 'Ya está '); (* Se acabó: avisamos, *)
close( fichero1 ); (* cerramos uno *)
close( fichero2 ); (* y el otro *)
end (* Final del "if" *)
else
writeln(' No he encontrado el fichero! '); (* Si no existe *)
end.
Ya hemos visto cómo acceder a los ficheros de texto, tanto para leerlos como para escribir
en ellos. Hoy nos centraremos en lo que vamos a llamar "ficheros con tipo".
Estos son ficheros en los que cada uno de los elementos que lo integran es del mismo tipo
(como vimos que ocurre en un array).
En los de texto se podría considerar que estaban formados por elementos iguales, de tipo
"char", pero ahora vamos a llegar más allá, porque un fichero formado por datos de tipo
"record" sería lo ideal para empezar a crear nuestra propia agenda.
Pues una vez que se conocen los ficheros de texto, no hay muchas diferencias a la hora
de un primer manejo: debemos declarar un fichero, asignarlo, abrirlo, trabajar con él y
cerrarlo.
Pero ahora podemos hacer más cosas también. Con los de texto, el uso habitual era leer
línea por línea, no carácter por carácter. Como las líneas pueden tener cualquier longitud,
no podíamos empezar por leer la línea 4, por ejemplo, sin haber leído antes las tres
anteriores. Esto es lo que se llama ACCESO SECUENCIAL.
Ahora sí que sabemos lo que va a ocupar cada dato, ya que todos son del mismo tipo, y
podremos aprovecharlo para acceder a una determinada posición del fichero cuando nos
interese, sin necesidad de pasar por todas las posiciones anteriores. Esto es el ACCESO
ALEATORIO (o directo).
La idea es sencilla: si cada ficha ocupa 25 bytes, y queremos leer la número 8, bastaría
con "saltarnos" 25*7=175 bytes.
Pero Turbo Pascal nos lo facilita más aún, con una orden, seek, que permite saltar a una
determinada posición de un fichero sin tener que calcular nada nosotros mismos. Veamos
un par de ejemplos...
program IntroduceDatos;
type
ficha = record (* Nuestras fichas *)
nombre: string [80];
edad: byte
end;
var
fichero: file of ficha; (* Nuestro fichero *)
bucle: byte; (* Para bucles, claro *)
datoActual: ficha; (* La ficha actual *)
begin
assign( fichero, '[Link]' ); (* Asignamos *)
rewrite( fichero ); (* Abrimos (escritura) *)
writeln(' Te iré pidiendo los datos de cuatro personas...' );
for bucle := 1 to 4 do (* Repetimos 4 veces *)
begin
writeln(' Introduce el nombre de la persona número ', bucle);
readln( [Link] );
writeln(' Introduce la edad de la persona número ', bucle);
readln( [Link] );
write( fichero, datoActual ); (* Guardamos el dato *)
end;
close( fichero ); (* Cerramos el fichero *)
end. (* Y se acabó *)
La única diferencia con lo que ya habíamos visto es que los datos son de tipo "record" y
que el fichero se declara de forma distinta, con "file of TipoBase".
Entonces ahora vamos a ver cómo leeríamos sólo la tercera ficha de este fichero de
datos que acabamos de crear:
program LeeUnDato;
type
ficha = record
nombre: string [80];
edad: byte
end;
var
fichero: file of ficha;
bucle: byte;
datoActual: ficha;
begin
assign( fichero, '[Link]' );
reset( fichero ); (* Abrimos (lectura) *)
seek( fichero, 2 ); (* <== Vamos a la ficha 3 *)
read( fichero, datoActual ); (* Leemos *)
writeln(' El nombre es: ', [Link] );
writeln(' La edad es: ',[Link] );
close( fichero ); (* Y cerramos el fichero *)
end.
El listado debe ser autoexplicativo. La única cosa que merece la pena comentar es eso del
"seek( fichero, 2 )": La posición de las fichas dentro de un fichero de empieza a numerar
en 0, que corresponderá a la primera posición. Así, accederemos a la segunda posición
con un 1, a la tercera con un 2, y en general a la "n" con "seek(fichero,n-1)".
Y ya que como mejor se aprende es practicando, queda propuesto elaborar una agenda:
En primer lugar, va a ser una agenda que guarde una sola ficha en memoria y que vaya
leyendo cada ficha que nos interese desde el disco, o escribiendo en él los nuevos datos
(todo ello de forma "automática", sin que quien maneje la agenda se de cuenta). Esto hace
que sea más lenta, pero no tiene más limitación de tamaño que el espacio libre en nuestro
disco duro. Las posibilidades que debe tener serán:
Más adelante ya le iremos introduciendo mejoras, como buscar, ordenar, imprimir una o
varias fichas, etc. El formato de cada ficha será:
• Nombre: 20 letras.
• Dirección: 30 letras.
• Ciudad: 15 letras.
• Código Postal: 5 letras.
• Teléfono: 12 letras.
• Observaciones: 40 letras.
Hemos visto cómo acceder a los ficheros de texto y a los ficheros "con tipo". Pero en la
práctica nos encontramos con muchos ficheros que no son de texto y que tampoco tienen
un tipo de datos claro.
Muchos formatos estándar como PCX, DBF o GIF están formados por una cabecera en la
que se dan detalles sobre el formato de los datos, y a continuación ya se detallan los datos
en sí.
La hay: declarar un fichero sin tipo, en el que nosotros mismos decidimos qué tipo de
datos queremos leer en cada momento.
donde
Hay otra diferencia con los ficheros que hemos visto hasta ahora, y es que cuando
abrimos un fichero sin tipo con "reset", debemos indicar el tamaño de cada dato
(normalmente diremos que 1, y así podemos leer variables más o menos grandes
indicándolo con el "cuantos" que aparece en BlockRead).
reset( fichero, 1 );
Los bloques que leemos con "BlockRead" deben tener un tamaño menor de 64K (el
resultado de multiplicar "cuantos" por el tamaño de cada dato).
Esta vez, es en "rewrite" (cuando abrimos el fichero para escritura) donde deberemos
indicar el tamaño de los datos (normalmente 1 byte).
Como las cosas se entienden mejor practicando, ahí va un primer ejemplo, tomado de la
ayuda en línea de Turbo Pascal y ligeramente retocado, que es un programa que copia un
fichero leyendo bloques de 2K:
program CopiaFichero;
var
Origen, Destino: file;
CantLeida, CantEscrita: Word;
NombreOrg, NombreDest: String;
Buffer: array[1..2048] of Char;
begin
Write( 'Introduzca el nombre del fichero ORIGEN... ' );
ReadLn( NombreOrg );
Write( 'Introduzca el nombre del fichero DESTINO... ' );
ReadLn( NombreDest );
Assign( Origen, NombreOrg );
Reset( Origen, 1 ); { Tamaño = 1 }
Assign( Destino, NombreDest );
Rewrite( Destino, 1 ); { Lo mismo }
WriteLn( 'Copiando ', FileSize(Origen), ' bytes...' );
repeat
BlockRead( Origen, Buffer, SizeOf(Buffer), CantLeida);
BlockWrite( Destino, Buffer, CantLeida, CantEscrita);
until (CantLeida = 0) or (CantEscrita <> CantLeida);
Close( Origen );
Close( Destino );
WriteLn( 'Terminado.' )
end.
Una mejora: es habitual usar "SizeOf" para calcular el tamaño de una variable, en vez de
calcularlo a mano y escribir, por ejemplo, 2048. Es más fiable y permite modificar el tipo o
el tamaño de la variable en la que almacenamos los datos leídos sin que eso repercuta en
el resto del programa.
program GifHeader;
Type
Gif_Header = Record { Primeros 13 Bytes de un Gif }
Firma, NumVer : Array[1..3] of Char;
Tam_X,
Tam_Y : Word;
_Packed,
Fondo,
Aspecto : Byte;
end;
Var
Fich : File;
Cabecera : GIF_Header;
Nombre: String;
begin
Write( '¿Nombre del fichero GIF (con extensión)? ');
ReadLn( Nombre );
Assign( Fich, Nombre );
Reset( Fich, 1 ); { Tamaño base: 1 byte }
Blockread( Fich, Cabecera, SizeOf(Cabecera) );
Close( Fich );
With Cabecera DO
begin
Writeln('Versión: ', Firma, NumVer);
Writeln('Resolución: ', Tam_X, 'x',
Tam_Y, 'x', 2 SHL (_Packed and 7));
end;
end.
Características:
- Número de fichas ilimitado, pero mayor lentitud, porque
los datos están permanentemente en disco, y en memoria se
almacena sólo la ficha actual.
- Muestra un ficha.
- Se puede ver la siguiente, la anterior o la número "x"
- Se pueden añadir fichas nuevas.
Posibles mejoras:
- Muuuuuchas. Por ejemplo...
- Mejorar la presentación.
- Mejorar la entrada de datos y su modificación.
- Buscar un determinado texto en las fichas.
- Imprimir una o varias fichas.
program MiniAg;
const
nombref: string[12]='[Link]'; { Nombre del fichero }
var
FichAgenda: file of tipoagenda; { Fichero }
ficha: TipoAgenda; { Guarda la ficha actual }
NumFicha: word; { El número de ficha actual }
Ultima: word; { Número de la última ficha }
opcion: char; { La opción del menú que se elige }
window(1,1,80,25);
gotoxy(1,25); { Para que el cursor no moleste }
pausa;
end;
readln( ciudad );
writeln; writeln('¿ Código Postal ?');
readln( cp );
writeln; writeln('¿ Teléfono ?');
readln( telef );
writeln; writeln('¿ Observaciones ?');
readln( observ );
end;
seek( FichAgenda, NumFicha-1 ); { Se sitúa }
write( FichAgenda,ficha ); { y escribe la ficha }
Ultima := Ultima + 1; { Ahora hay una más }
end;
writeln;
writeln('¿ Qué número de ficha ?');
readln( numero );
if numero>0 then { comprueba que sea válido }
if numero<=ultima then
NumFicha:=numero { si es <= que la última, salta }
else
NumFicha:=ultima; { si es mayor, se queda en la última }
end;
En su momentos, empleamos la unidad CRT, que nos daba una serie de facilidades para
manejar la pantalla en modo texto, el teclado y la generación de sonidos sencillos.
Iremos viendo otras unidades estándar cuando accedamos a la pantalla en modo gráfico,
a los servicios del DOS, etc. Pero por ahora vamos a ver cómo podemos crear las
nuestras propias.
¿Para qué? Nos podría bastar con teclear en un programa todas las funciones que nos
interesen. Si creamos otro programa que las necesite, pues las copiamos también en ese
y ya está...
• La primera ventaja es que los programas sean más modulares. Que podamos
dejar aparte las funciones que se encargan de batallar con el teclado, por ejemplo,
y en nuestro programa principal sólo esté lo que realmente tenga este programa
que lo diferencie de los otros. Esto facilita la legibilidad y con ello las posibles
correcciones o ampliaciones.
• La segunda ventaja es que no tenemos distintas versiones de los mismos
procedimientos o funciones. Esto ayuda a ganar espacio en el disco duro, pero eso
es lo menos importante. Lo realmente interesante es que si se nos ocurre una
mejora para un procedimiento, todos los programas que lo usen se van a beneficiar
de él automáticamente.
Una "unit" tiene dos partes: una pública, que es aquella a la que podremos acceder, y una
privada, que es el desarrollo detallado de esa parte pública, y a esta parte no se puede
acceder desde otros programas.
Debajo de interface basta indicar los nombres de los procedimientos que queremos
"exportar", así como las variables, si nos interesase crear alguna. Debajo de
implementation escribimos realmente estos procedimientos o funciones, tal como
haríamos en un programa normal.
Este ejemplo declara un procedimiento "AtXY" que hace un GotoXY y un Write en un solo
paso. Un programa que lo emplease podría ser simplemente:
program PruebaDeMiCrt1;
uses miCrt1;
begin
AtXY( 7, 5, 'Texto en la posición 7,5.' );
end.
Este programa no necesita llamar a la unidad CRT original, sino que nuestra unidad ya lo
hace por él. Vamos a mejorar ligeramente nuestra unidad, añadiéndole un procedimiento
"pausa":
{-------------------}
interface { Parte "pública", que se exporta }
{-------------------}
implementation { Parte "privada", detallada }
{-------------------}
end. { Final de la unidad }
y un programa que usase esta unidad, junto con la CRT original podría ser:
program PruebaDeMiCrt2;
begin
ClrScr; { De Crt }
atXY( 7, 5, 'Texto en la posición 7,5.' ); { de miCrt2 }
pausa; { de miCrt2 }
end.
program PruebaDeMiCrt3;
begin
ClrScr; { De Crt }
atXY( 7, 5, 'Texto en la posición 7,5.' ); { de miCrt3 }
if not EraMono then
atXY ( 10, 10, 'Modo de color ' );
pausa; { de miCrt3 }
end.
• Al compilar una unidad se crea un fichero .TPU, al que se puede acceder desde
nuestros programas con dos condiciones: que empleemos la misma versión de
Turbo Pascal (el formato de las TPU varía en cada versión), y que sepamos cómo
es la parte pública (interface).
• Cada unidad tiene su propio segmento de código (esto va para quien conozca la
estructura de la memoria en los PC), así que una unidad pueda almacenar hasta
64k de procedimientos o funciones. Los datos son comunes a todas las unidades,
con la limitación 64k en total (un segmento) para todos los datos (estáticos) de todo
el programa.
Si queremos almacenar datos de más de 64k en el programa, tenga una o más unidades,
deberemos emplear variables dinámicas, distintas en su manejo de las que hemos visto
hasta ahora (estáticas), pero eso ya lo veremos en el próximo tema...
En Pascal estándar, tal y como hemos visto hasta ahora, tenemos una serie de variables
que declaramos al principio del programa o de cada módulo (función o procedimiento,
unidad, etc.). Estas variables, que reciben el nombre de estáticas, tienen un tamaño
asignado desde el momento en que se crea el programa.
Esto es cómodo para detectar errores y rápido si vamos a manejar estructuras de datos
que no cambien, pero resulta poco eficiente si tenemos estructuras cuyo tamaño no sea
siempre el mismo.
Es el caso de una agenda: tenemos una serie de fichas, e iremos añadiendo más. Si
reservamos espacio para 10, no podremos llegar a añadir la número 11, estamos limitando
el máximo. En este caso, la solución que vimos fue la de trabajar siempre en el disco. No
tenemos límite en cuanto a número de fichas, pero es muchísimo más lento.
Lo ideal sería aprovechar mejor la memoria que tenemos en el ordenador, para guardar en
ella todas las fichas o al menos todas aquellas que quepan en memoria.
Una solución "típica" es sobredimensionar: preparar una agenda contando con 1000
fichas, aunque supongamos que no vamos a pasar de 200. Esto tiene varios
inconvenientes: se desperdicia memoria, obliga a conocer bien los datos con los que
vamos a trabajar, sigue pudiendo verse sobrepasado, y además en Turbo Pascal tenemos
muy poca memoria disponible para variables estáticas: 64K (un segmento, limitaciones
heredadas del manejo de memoria en el DOS en modo real).
Todas estas limitaciones se solucionan con el uso de variables dinámicas, para las cuales
se reserva espacio en el momento de ejecución del programa, sólo en la cantidad
necesaria, se pueden añadir elementos después, y se puede aprovechar toda la memoria
convencional (primeros 640K) de nuestro equipo.
Si además nuestro compilador genera programas en modo protegido del DOS, podremos
aprovechar toda la memoria real de nuestro ordenador (4 Mb, 8 Mb, etc.). Si crea
programas para sistemas operativos que utilicen memoria virtual (como OS/2 o Windows,
que destinan parte del disco duro para intercambiar con zonas de la memoria principal, de
modo que aparentemente tenemos más memoria disponible), podremos utilizar también
esa memoria de forma transparente para nosotros.
Así que se acabó la limitación de 64K. Ahora podremos tener, por ejemplo, 30 Mb de
datos en nuestro programa y con un acceso muchísimo más rápido que si teníamos las
fichas en disco, como hicimos antes.
Ahora "sólo" queda ver cómo utilizar estas variables dinámicas. Esto lo vamos a ver en 3
apartados. El primero (éste) será la introducción y veremos cómo utilizar arrays con
elementos que ocupen más de 64K. El segundo, manejaremos las "listas enlazadas". El
tercero nos centraremos en los "árboles binarios" y comentaremos cosas sobre otras
estructuras.
3.13.1 Introducción.
La idea de variable dinámica está muy relacionada con el concepto de puntero (pointer).
Un puntero es una variable que "apunta" a una determinada posición de memoria, en la
que se encuentran los datos que nos interesan.
Como un puntero almacena una dirección de memoria, sólo gastará 4 bytes de esos 64K
que teníamos para datos estáticos. El resto de la memoria (lo que realmente ocupan los
datos) se asigna en el momento en el que se ejecuta el programa y se toma del resto de
los 640K. Así, si nos quedan 500K libres, podríamos guardar cerca de 2000 fichas en
memoria, en vez de las 280 de antes. De los 64K del segmento de datos sólo estaríamos
ocupando cerca de 8K (2000 fichas x 4 bytes).
program Dinamicas;
type
pFicha = ^Ficha; (* Puntero a la ficha *)
var
fichero: file of ficha; (* El fichero, claro *)
datoLeido: ficha; (* Una ficha que se lee *)
indice: array [1..1000] of pFicha; (* Punteros a 1000 fichas *)
begin
assign( fichero, '[Link]' ); (* Asigna el fichero *)
reset( fichero ); (* Lo abre *)
for contador := 1 to 1000 do (* Va a leer 1000 fichas *)
begin
read( fichero, datoleido ); (* Lee cada una de ellas *)
new( indice[contador] ); (* Le reserva espacio *)
indice[contador]^ := datoLeido; (* Y lo guarda en memoria *)
end;
close( fichero ); (* Cierra el fichero *)
writeln('El nombre de la ficha 500 es: ');
writeln(indice[500]^.nombre);
for contador := 1 to 1000 do (* Liberamos memoria usada *)
dispose( indice[contador] );
end.
El acento circunflejo (^) quiere decir "que apunta a" o "apuntado por". Así,
pFicha = ^Ficha;
indice[500]^.nombre
será el campo nombre del dato al que apunta la dirección 500 del índice. El manejo es
muy parecido al de un array que contenga records, como ya habíamos visto, con la
diferencia de el carácter ^, que indica que se trata de punteros.
Antes de asignar un valor a una variable dinámica, hemos de reservarle espacio con
"new", porque si no estaríamos escribiendo en una posición de memoria que el compilador
no nos ha asegurado que esté vacía, y eso puede hacer que "machaquemos" otros datos,
o parte del propio programa, o del sistema operativo... esto es muy peligroso, y puede
provocar desde simples errores muy difíciles de localizar hasta un "cuelgue" en el
ordenador o cosas más peligrosas...
Cuando terminamos de utilizar una variable dinámica, debemos liberar la memoria que
habíamos reservado. Para ello empleamos la orden "dispose", que tiene una sintaxis igual
que la de new:
Ya hemos visto una forma de tener arrays de más de 64K de tamaño, pero seguimos con
la limitación en el número de fichas. En el próximo apartado veremos cómo evitar también
esto.
Tomando la base que vimos, vamos a hacer un lector de ficheros de texto. Algo parecido
al [Link] que incluyen Borland y otras casas en muchos de sus programas.
Hay cosas que se podrían hacer mejor, pero me he centrado en procurar que sea lo más
legible posible...
const
MaxLineas = 2000; { Para modificarlo fácilmente }
type
LineaTxt = string [80]; { Una línea de texto }
var
nomFich: string; { El nombre del fichero }
fichero: text; { El fichero en sí }
datos: lineas; { Los datos, claro }
lineaActual: string; { Cada línea que lee del fichero }
TotLineas: word; { El número total de líneas }
Primera: word; { La primera línea en pantalla }
procedure Lee;
begin;
clrscr;
TotLineas := 0; { Inicializa variables }
Primera := 0;
while (not eof(fichero)) { Mientras quede fichero }
and (TotLineas < MaxLineas) do { y espacio en el array }
begin
readln( fichero, LineaActual ); { Lee una línea }
TotLineas := TotLineas + 1 ; { Aumenta el contador }
new(datos[TotLineas]); { Reserva memoria }
datos[TotLineas]^ := LineaActual; { y guarda la línea }
end;
if TotLineas > 0 { Si realmente se han leído líneas }
then Primera := 1; { empezaremos en la primera }
close(fichero); { Al final, cierra el fichero }
end;
EscribeAbajo('Líneas:'+strs(Primera)+'-'+
strs(Primera+22)+'/'+strs(TotLineas));
tecla := readkey ;
if tecla = kbFuncion then begin { Si es tecla de función }
tecla := readkey; { Mira el segundo código }
case tecla of
kbArr: { Flecha arriba }
if Primera>1 then Primera := Primera -1;
kbAbj: { Flecha abajo }
if Primera<TotLineas-22 then Primera := Primera + 1;
kbPgArr: { Página arriba }
if Primera>22 then Primera := Primera - 22
else Primera := 1;
kbPgAbj: { Página Abajo }
if Primera< (TotLineas-22) then
Primera := Primera + min(22, TotLineas-23)
else Primera := TotLineas-22;
end;
end;
until tecla = kbEsc;
end;
begin
Inicio; { Pantalla inicial }
assign(fichero, nomFich); { Asigna el fichero }
{$I-} { desactiva errores de E/S }
reset(fichero); { e intenta abrirlo }
{$I+} { Vuelve a activar errores }
if IOresult = 0 then { Si no ha habido error }
begin
Pantalla; { Dibuja la pantalla }
Lee; { Lee el fichero }
Muestra; { Y lo muestra }
end
else { Si hubo error }
begin
writeln(' ¡ No se ha podido abrir el fichero ! '); { Avisa }
pausa;
end;
salir { En cualq. caso, sale al final }
end.
Habíamos comentado cómo podíamos evitar las limitaciones de 64K para datos y de tener
que dar un tamaño fijo a las variables del programa.
Después vimos con más detalle como podíamos hacer arrays de más de 64K.
Aprovechábamos mejor la memoria y a la vez seguíamos teniendo acceso directo a cada
dato. Como inconveniente: no podíamos añadir más datos que los que hubiéramos
previsto al principio (2000 líneas en el caso del lector de ficheros que vimos como
ejemplo).
Pues ahora vamos a ver dos tipos de estructuras totalmente dinámicas (frente a los arrays,
que eran estáticos). En esta lección serán las listas, y en la próxima trataremos los
árboles binarios. Hay otras muchas estructuras, pero no son difíciles de desarrollar si se
entienden bien estas dos.
Ahora "el truco" consistirá en que dentro de cada dato almacenaremos todo lo que nos
interesa, pero también una referencia que nos dirá dónde tenemos que ir a buscar el
siguiente.
(Posición: 1023).
Nombre : 'LuisCastellanos'
DireccionFido : '2:346/3.30'
SiguienteDato : 1430
Este dato está almacenado en la posición de memoria número 1023. En esa posición
guardamos el nombre y la dirección (o lo que nos interese) de esta persona, pero también
una información extra: la siguiente ficha se encuentra en la posición 1430.
Así, es muy cómodo recorrer la lista de forma secuencial, porque en todo momento
sabemos dónde está almacenado el siguiente dato. Cuando lleguemos a uno para el que
no esté definido cual es el siguiente, quiere decir que se ha acabado la lista.
Hemos perdido la ventaja del acceso directo: ya no podemos saltar directamente a la ficha
número 500. Pero, por contra, podemos tener tantas fichas como la memoria nos permita.
Para añadir un ficha, no tendríamos más que reservar la memoria para ella, y el Turbo
Pascal nos diría "le he encontrado sitio en la posición 4079". Así que nosotros iríamos a la
última ficha y le diríamos "tu siguiente dato va a estar en la posición 4079".
type
pFicha = ^Ficha; { Puntero a la ficha }
La diferencia está en el campo "siguiente" de nuestro registro, que es el que indica donde
se encuentra la ficha que va después de la actual.
Un puntero que "no apunta a ningún sitio" tiene el valor NIL, que nos servirá después para
comprobar si se trata del final de la lista: todas las fichas "apuntarán" a la siguiente, menos
la última, que "no tiene siguiente".
y la crearíamos con
Ahora podríamos añadir una ficha detrás de ella. Primero guardamos espacio para la
nueva ficha, como antes:
dato1^.siguiente := dato2;
new (dato3);
dato3^.nombre := 'Carlos';
dato3^.direccion := 'Por ahí';
dato3^.edad := 14;
dato3^.siguiente := dato2; { enlazamos con la siguiente }
o gráficamente:
Es decir: cada ficha está enlazada con la siguiente, salvo la última, que no está enlazada
con ninguna (apunta a NIL).
Hemos empleado tres variables para guardar tres datos. Si tenemos 20 datos,
¿necesitaremos 20 variables? ¿Y 3000 variables para 3000 datos?
Por ejemplo, un procedimiento que muestre en pantalla toda la lista se podría hacer de
forma recursiva así:
Aquí va un programa de ejemplo, que ordena los elementos que va insertando... y poco
más:
program EjemploDeListas;
type
puntero = ^TipoDatos;
TipoDatos = record
numero: integer;
sig: puntero
end;
end;
var
l: puntero; { Variables globales: la lista }
begin
l := CrearLista(5); { Crea una lista e introduce un 5 }
InsertaLista(l, 3); { Inserta un 3 }
InsertaLista(l, 2); { Inserta un 2 }
InsertaLista(l, 6); { Inserta un 6 }
MuestraLista(l) { Muestra la lista resultante }
end.
Ejercicios propuestos:
3- ¿Y uno de búsqueda, que devolviera la posición en la que está un dato, o NIL si el dato
no existe?
4- ¿Cómo se haría una lista "doblemente enlazada", que se pueda recorrer hacia adelante
y hacia atrás?
Hemos visto cómo crear listas dinámicas enlazadas, y cómo podíamos ir insertando los
elementos en ellas de forma que siempre estuviesen ordenadas.
• Una cola es otro caso particular, en el que los elementos se introducen por un
extremo y se sacan por el otro. Es como se supone que debería ser la cola del cine:
los que llegan, se ponen al final, y se atiende primero a los que están al principio.
Esta es una estructura FIFO (First In, First Out).
Estas dos son estructuras más sencillas de programar de lo que sería una lista en su caso
general, pero que son también útiles en muchos casos.
Finalmente, antes de pasar con los "árboles", comentaré una mejora a estas listas
enlazadas que hemos visto. Tal y como las hemos tratado, tienen la ventaja de que no hay
limitaciones tan rígidas en cuanto a tamaño como en las variables estáticas, ni hay por qué
saber el número de elementos desde el principio. Pero siempre hay que recorrerlas desde
DELANTE hacia ATRÁS, lo que puede resultar lento. Una mejora relativamente evidente
es lo que se llama una lista doble o lista doblemente enlazada: si guardamos punteros al
ÁRBOLES.
En primer lugar, veamos de donde viene el nombre. En las listas, después de cada
elemento venía otro (o ninguno, si habíamos llegado al final). Pero también nos puede
interesar tener varias posibilidades después de cada elemento, 3 por ejemplo. De cada
uno de estos 3 saldrían otros 3, y así sucesivamente. Obtendríamos algo que recuerda a
un árbol: un tronco del que nacen 3 ramas, que a su veces se subdividen en otras 3 de
menor tamaño, y así sucesivamente hasta llegar a las hojas.
Pues eso será un árbol: una estructura dinámica en la que cada nodo (elemento) puede
tener más de un "siguiente". Nos centraremos en los árboles binarios, en los que cada
nodo puede tener un hijo izquierdo, un hijo derecho, ambos o ninguno (dos hijos como
máximo).
Para puntualizar a un más, aviso que trataremos los árboles binarios de búsqueda, en los
que tenemos prefijado un cierto orden, que nos ayudará a encontrar un cierto dato dentro
de un árbol con mucha rapidez.
¿Y como es este "orden prefijado"? Sencillo: para cada nodo tendremos que:
Como ejemplo, vamos a introducir en un árbol binario de búsqueda los datos 5,3,7,2,4,8,9
5
/
3
5
/ \
3 7
5
/ \
3 7
/
2
5
/ \
3 7
/ \
2 4
5
/ \
3 7
/ \ \
2 4 8
5
/ \
3 7
/ \ \
2 4 8
\
9
¿Y qué ventajas tiene esto? Pues la rapidez: tenemos 7 elementos, lo que en una lista
supone que si buscamos un dato que casualmente está al final, haremos 7
comparaciones; en este árbol, tenemos 4 alturas => 4 comparaciones como máximo.
De este modo, el número máximo de comparaciones que tendríamos que hacer sería
log2(n), lo que supone que si tenemos 1000 datos, en una lista podríamos llegar a tener
que hacer 1000 comparaciones, y en un árbol binario, log2(1000) => 10 comparaciones
como máximo. La ganancia es evidente.
No vamos a ver cómo se hace eso de los "equilibrados", que sería propio de un curso de
programación más avanzado, o incluso de uno de "Tipos Abstractos de Datos" o de
"Algorítmica", y vamos a empezar a ver rutinas para manejar estos árboles binarios de
búsqueda.
Recordemos que la idea importante es todo dato menor estará a la izquierda del nodo que
miramos, y los datos mayores estarán a su derecha.
type
Y las rutinas de inserción, búsqueda, escritura, borrado, etc., podrán ser recursivas. Como
primer ejemplo, la de escritura de todo el árbol (la más sencilla) sería:
Si alguien no se cree que funciona, que coja lápiz y papel y lo compruebe con el árbol que
hemos puesto antes como ejemplo. Es muy importante que este procedimiento quede
claro antes de seguir leyendo, porque los demás serán muy parecidos.
Un comentario: esta última rutina es peligrosa, porque indicamos que "punt" está libre y
después miramos cual es su hijo izquierdo (después de haber borrado la variable). Esto
funciona en Turbo Pascal, porque marca esa zona de memoria como disponible pero no la
borra físicamente, pero puede dar problemas con otros compiladores o si se adapta esta
rutina a otros lenguajes (como C). Una forma más segura de hacer lo anterior sería:
begin
if punt <> nil then { Si queda algo que borrar }
begin
BorrarArbol2(punt^.hijoIzq); { Borra la izqda recursivamente }
O bien, simplemente, se pueden borrar recursivamente los dos hijos antes que el padre
(ahora ya no hace falta ir "en orden", porque no estamos leyendo, sino borrando todo):
Ejercicios propuestos:
En Turbo Pascal 7.0, tenemos dos órdenes extra para el control de bucles, tanto si se trata
de "for", como de "repeat" o "until". Estas órdenes son:
Un ejemplo "poco útil" que use ambas podría ser escribir los números pares hasta el 10
con un for:
Esto se puede imitar en versiones anteriores de TP con el temible "goto", que vamos a
comentar a continuación:
3.14.2 Goto.
Es una orden que se puede emplear para realizar saltos incondicionales. Su empleo está
bastante desaconsejado, pero en ciertas ocasiones, y se lleva cuidado, puede ser útil
(para salir de bucles fuertemente anidados, por ejemplo, especialmente si no se dispone
de las dos órdenes anteriores.
El formato es
goto etiqueta
y las etiquetas se deben declarar al principio del programa, igual que las variables. Para
ello se usa la palabra "label". Vamos a verlo directamente con un ejemplo:
var
donde: byte;
begin
writeln('¿Quiere saltar a la opción 1 o a la 2?');
readln (donde);
case donde of
1: goto uno;
2: goto dos;
else writeln('Número incorrecto');
end;
exit;
uno:
writeln('Esta es la opción 1. Vamos a seguir...');
dos:
Para esto usamos "getmem" y "freemem", en los que debemos indicar cuánta memoria
queremos reservar para el dato en cuestión. Si queremos utilizarlos como new y dispose,
reservando toda la memoria que ocupa el dato (será lo habitual), podemos usar "sizeof"
para que el compilador lo calcule por nosotros:
type
TipoDato = record
Nombre: string[40];
Edad: Byte;
end;
var
p: pointer;
begin
if MaxAvail < SizeOf(TipoDato) then
Writeln('No hay memoria suficiente')
else
begin
GetMem(p, SizeOf(TipoDato));
{ Trabajaríamos con el dato, y después... }
FreeMem(p, SizeOf(TipoDato));
end;
end.
Por cierto, "pointer" es un puntero genérico, que no está asociado a ningún tipo concreto
de dato. No lo vimos en la lección sobre punteros, porque para nosotros lo habitual es
trabajar con punteros que están relacionados con un cierto tipo de datos.
En temas anteriores vimos que podíamos usar exit para salir de un programa.
Esto no es exacto del todo: exit sale del bloque en el que nos encontremos, que puede ser
un procedimiento o una función. Por tanto, sólo sale del programa si ejecutamos exit
desde el cuerpo del programa principal.
Halt sí que abandona el programa por completo, estemos donde estemos. Como
parámetro se le puede pasar el "Errorlevel" que se quiere devolver al DOS, y que se
puede leer desde un fichero batch.
Por ejemplo: "halt" o "halt(0)" indicaría una salida normal del programa (sin errores),
"halt(1)" podría indicar que no se han encontrado los datos necesarios, etc.
var i:integer;
begin
Randomize;
for i := 1 to 50 do
Write (Random(1000), ' ');
end.
3.14.6 Funciones.
La mayoría de las que vamos a ver son funciones matemáticas que están ya predefinidas
en Pascal. Muchas de ellas son muy evidentes, pero precisamente por eso no podíamos
dejarlas sin mencionar al menos:
begin
writeln(elevado(2,3));
end.
La deducción de esta fórmula es fácil, conociendo las propiedades de los logaritmos y las
exponenciales.
4 Bibliografía
• [Link]
• [Link] (Curso de Pascal en Lìnea)
• [Link] (Clasificación Lenguajes de
Programación)
• [Link] (Conceptos de Lenguajes de Programación)