Descargar

Manual de FreePascal (Parte 1) (página 3)

Enviado por rafaelfreites


Partes: 1, 2, 3, 4, 5, 6

begin

for contador := 9 to 0 do

begin

Writeln('Hola'); // Esto no se ejecutará nunca

end;

end.

Como ejemplo final vamos a codificar un programa que calcule la suma de potencias de forma manual y que compruebe que el valor coincide con el de la fórmula antes mostrada, r^0 +r^1 +r^2 …+ r^n-1 = (r^n -1)/ (r-1)

program Calcular_n_potencias;

var

r, n, i, suma : Integer;

begin

Writeln('R :');

Readln(r);

if r > 1 then

begin

Writeln('N :');

Readln(n);

Writeln('Resultado :');

Writeln((r**n-1) div (r-1));

// Método mecánico

suma := 0;

for i := 0 to n-1 do

begin

suma := suma + r**i;

end;

Writeln('Resultado Mecánico :');

Writeln(suma);

end

else

begin

Writeln('R tiene que ser un valor mayor que 1');

end;

end.

Estructuras iterativas condicionales

Las estructuras iterativas condicionales basan su potencial en que el bucle finaliza en función del valor que toma una expresión booleana.

El bucle while..do

Este bucle no finaliza hasta que la condición que ha permitido iniciarlo se vuelve falsa. La estructura de while..do es la siguiente.

while expresionBooleana do Sentencia;

Veamos de ejemplo, el programa siguiente. Este programa no finaliza hasta que el usuario no ha introducido un punto como cadena de entrada.

var

cadena : string;

begin

cadena := ; // Una cadena vacía

while cadena <> '.' do

begin

Write('Escriba algo (Un punto para terminar) : ');

Readln(cadena);

end;

Writeln('Fin del programa');

end.

El bucle while ejecuta la sentencia o sentencias hasta que la condición del while se vuelve falsa. Hasta que no se ha terminado un ciclo no se evalúa de nuevo la condición, por tanto, aunque en medio del código la condición ya sea falsa se seguirán ejecutando sentencias hasta que al finalizar el bloque se tenga que repetir el bucle. Entonces la evaluación daría falso y el código seguiría al punto siguiente de después de la sentencias.

En el ejemplo anterior, es una expresión booleana que se vuelve falsa cuando cadena es igual a un punto. Cuando el usuario introduce un punto, el bucle ya no se repite y el usuario ve impreso en la pantalla "Fin del programa".

Si la condición del bucle ya fuera falsa al iniciarse por primera vez la sentencia while, el bucle ya no empezaría. El código siguiente, por ejemplo, no se ejecutaría nunca :

while false do

begin

Writeln('Esto no se ejecutará nunca');

end;

El literal false denota una expresión booleana falsa, por lo que el bucle no empezará nunca. Vemos el caso siguiente :

while true do

begin

Writeln('Esto no terminará nunca…');

end;

En este caso la expresión es siempre cierta por tanto el bucle sería infinito. De momento no sabemos como salir de un bucle infinito. Es importante asegurase de que la condición de salida se cumplirá alguna vez cuando diseñemos el algoritmo.

El bucle repeat..until

El bucle repeat..until es más o menos el contrario del bucle while..do. El bucle repeat..until, va ejecutando sentencias hasta que la condición del bucle es cierta. Otra diferencia se encuentra en e l hecho de que la evaluación de la condición del bucle se hace al final del bucle y no al principio de éste. Esto provoca que el código se ejecute al menos una vez antes de salir del bucle. Cuando la condición del bucle se cumple entonces éste termina.

Algunos autores consideran el uso de repeat..until inapropiado para una buena programación. En cualquier caso, lo cierto, es que toda estructura de tipo repeat..until es convertible en una estructura de tipo while..do. Veamos un ejemplo de repeat..until en el siguiente programa :

var

nombre : string;

begin

repeat

Write('Escriba su nombre : ');

Readln(nombre);

until nombre<>;

end.

En este caso podemos observar que si el usuario pulsa Intro sin haber introducido nada la variable nombre no es diferente de la cadena vacía () y por tanto el bucle vuelve a empezar.

Tal como hemos comentado anteriormente todos los repeat..until son transformables a while..do. La versión con while..do del programa anterior sería la siguiente :

var

nombre : string;

begin

nombre := ;

while nombre = do

begin

Write('Escriba su nombre : ');

Readln(nombre);

end;

end.

En este caso hay que asegurarse de que el código del bucle se ejecutará al menos una vez. Para esto, realizamos la asignación nombre := para que la condición del bucle while se cumpla al menos la primera vez.

Las funciones break y continue.

Algunas veces, necesitaremos alterar el flujo de ejecución de bucles for, while o repeat. Para este cometido disponemos de las funciones break y continue.

La función break hace que el programa salga del bucle y la función continue hace que el bucle procese la siguiente iteración sin terminar la iteración actual. Estas funciones sólo se pueden emplea r dentro de bucles for, while y repeat y no se pueden emplear fuera de estos ámbitos.

for numero := 1 to 10 do

begin

Writeln('Hola #', numero);

Continue;

Writeln('Adiós');

end;

while true do

begin

if Salir then break;

end;

En el primer caso 'Adiós' no se verá impreso nunca en la pantalla pues el bucle continúa con la siguiente iteración aunque el bloque de sentencias no haya terminado.

En el segundo ejemplo podemos salir del bucle infinito mediante una condición de salida Salir, que es booleana, y la función break.

Hay que tener en cuenta de que aunque es posible poner sentencias después de un break o continue, si estos forman parte de un mismo bloque de sentencias éstas ya no se ejecutarán, tal como hemos visto en el ejemplo anterior.

El selector case

Muchas veces necesitaremos modificar el comportamiento de un programa en función a los valores que toma una variable. Con lo que conocemos de Pascal, la única forma de hacerlo sería mediante if. Pero si la variable en cuestión puede tomar varios valores y necesitamos diferentes comportamientos entonces la sentencia if es algo limitada. Siempre podríamos imaginar el caso siguiente :

var

diasemana : Integer;

begin

Write('Introduzca el día de la semana : ');

Readln(diasemana);

if diasemana > 7 then begin Writeln('Número no válido') else

if diasemana = 1 then begin Writeln('Lunes') else

if diasemana = 2 then begin Writeln('Martes')

if diasemana = 7 then begin

Writeln('Domingo');

end.

Como se adivinará, esta no es la mejor forma de resolver este problema y termina por confundir al programador. Para este tipo de casos Pascal posee la estructura selectora case. Case sólo puede trabajar con ordinales (booleanos, caracteres y enteros) pero es una forma muy elegante de implementar lo que queremos. La estructura de case es algo más enrevesada que las anteriores pero muy sencilla a la larga :

Case expresionOrdinal of

Constante1 : Sentencia1;

Constante2 : Sentencia2;

ConstanteN : SentenciaN;

else SentenciaElse;

end;

Pasemos a ver un ejemplo de como se implementaría el problema de los días de la semana con un case :

var

diasemana : Integer;

begin

Write('Introduzca el día de la semana : ');

Readln(diasemana);

case diasemana of

1 : begin

Writeln('Lunes');

end;

2 : begin

Writeln('Martes');

end;

3 : begin

Writeln('Miércoles');

end;

4 : begin

Writeln('Jueves');

end;

5 : begin

Writeln('Viernes');

end;

6 : begin

Writeln('Sábado');

end;

7 : begin

Writeln('Domingo');

end;

else

begin

Writeln('Número no válido');

end;

end;

end.

La estructura introducida por else se ejecutará si el valor de diasemana no tiene un selector, por ejemplo si introducimos un 0 o un 8.

Hay que tener en cuenta varias cuestiones a la hora de escribir un case. Los valores ordinales de cada selector tienen que ser valores literales o valores constantes o una expresión evaluable en tiempo de compilación. Así, no se permiten las variables. Por ejemplo :

var

Numero1, Numero2 : Integer;

const Numero4 = 4;

begin

Numero1 := 3;

case Numero2 of

Numero1 : begin

… // Esta construcción no es valida

end;

Numero4 : begin

… // Esta sí que lo es

end;

Numero4+1 : begin

… // Esta también lo es

end;

end;

end.

Numero4+1 es válido pues es una expresión evaluable en tiempo de compilación, en nuestro caso tiene el valor 5. Como se puede ver, la estructura else se puede omitir sin ningún problema. En este caso si Numero2 tiene un valor sin selector no se hace nada. En una estructura case es posible especificar más de un tipo ordinal para cada selector, separándolos entre comas, o bien especificando un rango ordinal para el selector, separados con dos puntos seguidos (..).

Case Numero of

0..10 : begin

// Esto se ejecutaría con numero del 0 al 10 incluidos

end;

11, 13 : begin

// Sólo los valores 11 y 13

end;

12 : begin // Sólo el valor 12

end;

14..80, 82..90 : begin

// Los valores de 14 a 80 y de 82 a 90

end;

81, 91..100 : begin

// El valor 81 y del 91 al 100

end;

end;

No importa el orden en el que escribamos los selectores. Lo importante es que no se repitan, si fuera el caso el compilador nos advertiría del fallo. Este es uno de los motivos por los cuales los valores de los selectores tienen que ser evaluables en tiempo de compilación y no de ejecución.

En todos los ejemplos anteriores hemos empleado enteros ya que son ordinales por excelencia pero también podemos emplear booleanos y caracteres, que como sabemos también son ordinales.

uses Crt;

var

caracter : Char;

begin

Write('Introduzca un carácter…');

caracter := Readkey;

case caracter of

'A'..'Z', 'a'..'z' : begin Writeln('Es una letra');

end;

'0'..'9' : begin

Writeln('Es un número');

end;

else begin

Writeln('Otro tipo de carácter');

end;

end.

La función ReadKey es una función de la unit Crt que nos devuelve el carácter de la tecla que el usuario ha pulsado.

Tipos de datos estructurados

Los datos estructurados

Hasta el momento hemos trabajado todo el rato con tipos de datos básicos de Pascal : booleanos, caracteres, string, enteros y reales. La mayoría de aplicaciones de la vida real no emplean datos de uno en uno sino que emplean grupos de datos. Para resolver este problema Pascal incorpora un conjunto de datos estructurados : array (vectores o tablas), registros y conjuntos. También son tipos de datos estructurados las clases y los objetos pero que debido a su complejidad trataremos en otros capítulos. Finalmente, los archivos también son un tipo especial de datos estructurados que trataremos aparte.

Declaraciones de tipo

Antes hemos hablado de tipos fundamentales, pero también es posible definir nuevos tipos de datos que podremos utilizar en nuestras variables. Con la palabra reservada type podemos declarar nuevos tipos que podremos emplear cuando declaremos variables, de forma que estas variables sean del tipo definido. Veamos un ejemplo asaz inútil pero que servirá para introducirse a los tipos de datos. Los tipos se tendrían que declarar antes de las variables pero dentro de la declaración de tipo no es necesario seguir un orden especial.

type

Numero = Integer;

NumeroNoIdentico = type Integer;

var

N1 : Numero; // Esto es lo mismo que un Integer

N2 : NumeroNoIdentico; // Esto no es exactamente un Integer aunque funciona igual

begin

end.

Como se puede ver, la declaración de un tipo es semejante a la declaración de una constante. En el primer caso Numero no es más que otro nombre para Integer mientras que en el segundo caso NumeroNoIdentico es un símbolo nuevo para el compilador. Este último caso sirve para forzar al compilador que sea capaz de distinguir entre un Integer y NumeroNoIdentico mientras que Numero es imposible de distinguir de Integer. Es un caso algo especial y se emplea pocas veces. Sólo veremos este ejemplo.

Enumeraciones

El tipo enumerado permite construir datos con propiedades ordinales. La característica del tipo enumerado es que sólo puede tomar los valores de su enumeración.

type

TMedioTransporte : (Pie, Bicicleta, Moto, Coche, Camion, Tren, Barco, Avion);

var

MedioTransporte : TMedioTransporte;

begin

MedioTransporte := Camion;

{ Sólo es posible asignar a identificadores de la enumeración }

case MedioTransporte of // Es posible ya que un enumerado es un tipo ordinal

Pie : begin

end;

Bicicleta : begin

end;

end;

end.

Por convención, los tipos de datos se suelen declarar con una T seguido del nombre. De esta forma, los identificadores que empiezan por T se entiende que son tipos y no variables.

Las enumeraciones se declaran con paréntesis y un seguido de identificadores separados por comas la utilidad más importante de las enumeraciones se encuentra especialmente en los conjuntos.

Conjuntos

Un conjunto es una colección de elementos de un mismo tipo ordinal. De esta forma podemos añadir y suprimir elementos de un conjunto, comprobar si un elemento determinado pertenece al conjunto, etc.

Los conjuntos, además, permiten realizar operaciones propias de conjuntos como la unión, con el operador suma, o la intersección con la operación producto.

Los literales de conjuntos se expresan con claudátores y los identificadores del conjunto. Si deseamos más de uno hay que separarlos entre comas. El literal de conjunto vacío es [].

Dado el conjunto siguiente de comida rápida y dos variables de tipo de este conjunto definimos las operaciones siguientes :

type

TJunkFood = (Pizza, Donut, Frankfurt, Burguer); // Enumeración

TSetJunkFood = set of TJunkFood; // Conjunto de la enumeración anterior

var

JunkFood, JunkFood2 : TSetJunkFood;

Operación

Sintaxis

Qué hace?

Asignación directa

JunkFood := [Pizza, Donut]; Si queremos el conjunto vacío : JunkFood := [];

Asigna un conjunto de valores al conjunto de forma que estos pertenezcan a él.

Unión de conjuntos o elementos.

Si añadimos elementos: JunkFood := JunkFood + [Burguer]; Si unimos conjuntos del mismo tipo : JunkFood := JunkFood + JunkFood2;

Une un conjunto con otro de forma que el resultado es el conjunto unión.

Diferencia

Si suprimimos elementos : JunkFood := JunkFood – [Burguer]; Suprimimos los elementos de JunkFood2 que hay en JunkFood : JunkFood := JunkFood – JunkFood2;

Suprime los elementos del segundo conjunto que se encuentran en el primero y devuelve el conjunto resultado.

Intersección

Intersección de un conjunto y un literal : JunkFood := JunkFood * [Burguer]; (Si en JunkFood no hubiera Burguer obtendríamos el conjunto vacío). Intersección de dos conjuntos. JunkFood := JunkFood * JunkFood2;

Devuelve un conjunto que contiene los elementos que pertenecen a ambos

conjuntos intersecados.

Para comprobar que un elemento pertenece a un determinado conjunto hay que emplear el operador in :

if Burguer in JunkFood then { este operador devuelve cierto si en JunkFood hay el elemento Burguer }

begin

end;

Arrays o vectores

Llamamos array, vector o tabla a una estructura de datos formada por un conjunto de variables del mismo tipo a los cuales podemos acceder mediante uno o más índices.

Se declaran de la forma siguiente :

array [minElemento..maxElemento] of tipoDato;

Por ejemplo, en el caso siguiente :

var

MiArray : array [1..9] of Integer;

Hemos declarado un array de 9 elementos, del 1 al 9, al cual podemos acceder en un momento determinado del programa de la forma siguiente :

begin

MiArray[2] := 100;

MiArray[3] := 23;

MiArray[4] := MiArray[2] + MiArray[3];

end;

Hay que ir con cuidado a la hora de leer un elemento del array porque puede ser que nos estemos leyendo un punto que no está en el array :

begin

MiArray[10] := 4;// Error !!!

end.

Para saber el elemento máximo y mínimo de un array podemos emplear las funciones High y Low sobre el array :

begin

Writeln(Low(MiArray)); // Devuelve 1

Writeln(High(MiArray)); // Devuelve 9

end.

Podemos definir arrays multidimensionales concatenando dos rangos, por ejemplo, definiremos dos arrays de Integer. El primero será bidimensional de 3×2 y el segundo tridimensional de 4x3x2.

var

Bidimensional : array [1..3, 1..2] of Integer;

Tridimensional : array [1..4, 1..3, 1..2] of Integer;

Llegados aquí puede ser algo complicado entender como es un array bidimensional o un array multidimensional. Es fácil de interpretar. Cada elemento del array indicado por el primer índice es otro array el cual podemos acceder a sus elementos mediante la combinación del primer índice y el segundo. Por ejemplo si hacemos Bidimensional[2] estamos accediendo al segundo array de 2 elementos. Podemos acceder al primer elemento de este array de 2 elementos mediante la construcción siguiente :

Bidimensional[2, 1]

Bidimensional[1]

Bidimensional[1,1]

Bidimensional[1,2]

Bidimensional[2]

Bidimensional[2,1]

Bidimensional[2,2]

Bidimensional[3]

Bidimensional[3,1]

Bidimensional[3,2]

 

En este caso, el segundo índice sólo es valido para los valores 1 y 2. Obsérvese que si hace Low y High directamente a Bidimensional obtendrá 1 y 3, respectivamente, mientras que si los aplica a Bidimensional[1] obtendrá 1 y 2.

En el caso de que necesitemos asignar el contenido de un array a otro sólo es posible cuando tengan el mismo tipo estrictamente, hay que tener en cuenta que para el compilador igual declaración no implica el mismo tipo. Véase el programa siguiente :

program CopiarArray;

type

TMiArray : array [0..3] of Integer;

var

Array1 : TMiArray;

Array2 : TMiArray;

Array3 : array [0..3] of Integer;

Array4 : array [0..3] of Integer;

Array5, Array6 : array [0..3] of Integer;

begin

Array1 := Array2; // Compatible ya que el tipo es el mismo

Array3 := Array4; { Incompatible, tiene la misma declaración pero diferente tipo. Esto da un error de compilación }

Array5 := Array6; { Compatible, la declaración obliga al compilador a entender que las dos variables son idénticas }

end.

En los ejemplos hemos empleado rangos numéricos con enteros positivos pero también es posible definir el rango del array con números negativos como por ejemplo [-2..2] o bien [-5..-3]. Es importante recordar que el rango inicial no puede ser nunca superior al final. Por tanto un array con rango [5..3] es incorrecto.

Los string y los arrays*

Es posible acceder a los caracteres de un string (tanto si es ShortString, AnsiString o String[n]) mediante una sintaxis de array. Los string están indexados en cero pero el primer carácter se encuentra en la posición 1.

Cadena := 'Hola buenos días';

Writeln(Cadena[2]); {o}

Writeln(Cadena[14]); {í}

El nombre de caracteres de una cadena se puede obtener con la función Length. El valor que devuelve es el nombre de caracteres dinámicos, o sea el nombre de caracteres que tiene la cadena. En cambio no devuelve el máximo de caracteres que puede obtener la cadena :

var

Cadena : string[40];

begin

Cadena := 'Hola buenos días';

Writeln(Length(Cadena)); // Devuelve 16

end.

Para saber la longitud máxima de la cadena hay que emplear la función High. La función Low siempre devuelve cero con una cadena. Obsérvese que los elementos de una cadena son caracteres y por tanto podemos asignar a un Char el elemento de una cadena.

Registros o records

Un registro es un tipo de dato que contiene un conjunto de campos que no son más que un conjunto de identificadores reunidos bajo un mismo nombre.

La declaración de un registro es de la forma siguiente :

record

Variable1 : Tipo1;.

VariableN : TipoN;

end;

Por ejemplo si queremos almacenar datos referentes a un cliente podemos emplear el registro siguiente :

type

TDatosCliente = record

Nombre : string;

Apellidos : string;

NumTelefono : Integer;

end;

Ahora sólo hay que declarar una variable de este tipo y emplear sus campos :

var

DatosCliente : TDatosCliente;

begin

Writeln('Introduzca los datos del cliente');

Write('Nombre : '); Readln(DatosCliente.Nombre);

Write('Apellidos : '); Readln(DatosCliente.Apellidos);

Write('Nm. Teléfono : '); Readln(DatosCliente.NumTelefono);

end.

Como se puede ver, empleamos el punto para distinguir con qué variable queremos trabajar. Así DatosCliente.Nombre representa el campo Nombre que como hemos visto en la declaración es de tipo String. Téngase en cuenta que DatosCliente es de tipo TDatosCliente pero que los campos de éste tienen su propio tipo, tal como se ha declarado en el registro.

Podemos indicar al compilador que los identificadores que no conozca los busque en el record mediante la palabra reservada with. Podíamos haber escrito el código así :

var

DatosCliente : TDatosCliente;

begin

Writeln('Introduzca los datos del cliente');

with DatosCliente do

begin

Write('Nombre : '); Readln(Nombre); // Esto sólo puede ser

DatosCliente.Nombre

Write('Apellidos : '); Readln(Apellidos); // DatosCliente.Apellidos

Write('Nm. Teléfono : '); Readln(NumTelefono); // DatosCliente.NumTelefono

end;

end.

A menos que sea necesario, no se recomienda el uso de with ya que confunde al programador. La utilización del punto permite cualificar el identificador y evita confusiones. También podemos cualificar el identificador en función de su unit. Por ejemplo la función ReadKey de la unit Crt se puede indicar como Crt.ReadKey. En este caso, el punto es útil para determinar exactamente qué variable o qué función estamos llamando, especialmente cuando haya más de una en diferentes units.

[]

Registros variantes

Pascal permite definir lo que se llaman registros con partes variantes. Un registro con una parte variante consta de un campo que llamaremos selector. El valor de este selector es útil para el programador pues le permite saber cual de los campos posibles se están empleando. El compilador no tiene en cuenta el valor de este campo selector ya que permite el acceso a todos los campos declarados en la parte variante.

type

TConjuntoDatos = ( TCaracter, TCadena, TNumero, TCadenayCaracter );

TCombinado = record

case ConjuntoDatos : TConjuntoDatos of

TCaracter : ( Caracter : Char);

TCadena : ( Cadena : string );

TNumero : ( Numero : integer);

end;

var

Combinado : TCombinado;

Por ejemplo podemos realizar lo siguiente Combinado.Caracter := 'z'; y después Combinado.ConjuntoDatos := TCaracter; para indicar qué campo hay que leer. Por ejemplo, si hacemos la asignación Combinado.Numero := 65; y después hacemos Writeln(Combinado.Caracter); se escribirá en la pantalla la letra A.

Este comportamiento, en apariencia tan raro, es debido a que un registro con la parte variante sita los distintos campos en una misma región de memoria de forma que podamos trabajar con un Byte y un Boolean o trabajar con un Cardinal y leer sus dos Word, el superior y el inferior.

El uso de registros variantes está restringido a usos muy concretos. Esta es la forma Pascal de implementar un unión de C/C++.

Archivos

[editar]

La necesidad de los archivos

Los programas que hemos desarrollado hasta ahora se caracterizan por poseer una memoria volátil que se concretaba con las variables. Al finalizar el programa, el valor de las variables, sea útil o no, se pierde sin que haya forma de recuperarlo en una posterior ejecución del mismo programa. Los archivos permiten resolver este problema ya que permiten el almacenaje de datos en soportes no volátiles, como son los disquetes, los discos duros y su posterior acceso y modificación.

Cómo trabajar con archivos en Pascal

Todos los sistemas de archivos permiten asignar un nombre a los archivos de forma que lo podamos identificar de forma única dentro de un directorio, por ejemplo.

En Pascal, en vez de operar directamente con los nombres de archivos trabajaremos con un alias que no es nada más que una variable que ha sido asignada a un nombre de archivo. Las posteriores operaciones que queramos llevar a cabo sobre este archivo tomarán como parámetro este alias y todas las operaciones se realizarán al archivo al cual se refiere este alias.

Tipos de archivos en Pascal

Los tres tipos de archivos con los cuales podemos trabajar en Pascal son : archivos con tipo, archivo sin tipo y archivos de texto.

Los archivos con tipo almacenan un único tipo de dato y permiten los que se llama acceso aleatorio, en contraposición con el acceso secuencial que obliga a leer (o al menos pasar) por todos los datos anteriores a uno concreto. De esta forma podemos situarnos en cualquier parte del archivo de forma rápida y leer o escribir datos.

Los archivos sin tipo no almacenan, a la contra, ningún tipo en concreto. El programador es quien decide qué datos se leen y en qué orden se leen. El acceso a datos de este tipo de archivos es totalmente secuencial aunque es más versátil pues se permiten almacenar datos de casi cualquier tipo.

Los archivos de texto permiten la lectura y escritura de archivos ASCII y la conversión automática a cadenas String[255], o ShortString, que en realidad no son cadenas ASCII. El acceso a estos archivos también es secuencial.

[editar]

Trabajar con archivos de texto

Empezaremos con archivos de texto pues son más simples que otros tipos de archivo.

El tipo de archivo de texto se define con el tipo Text. Por tanto declarar un alias de archivo de texto es tan simple como la declaración siguiente :

var

ArchivoTexto : Text;

Para asignar a un alias un nombre de archivo emplearemos la función Assign^2 <#sdfootnote2sym>

Assign(ArchivoTexto, 'PRUEBA.TXT');

En este ejemplo hemos asignado la variable ArchivoTexto al archivo PRUEBA.TXT. Hay que tener en cuenta de que en Windows los archivos no son sensibles a las mayúsculas, podíamos haber puesto prueba.txt y sería el mismo archivo. Téngase en cuenta que en algunos sistemas operativos (como Linux) esto puede no ser cierto.

Ahora habrá que abrir el archivo para realizar alguna operación. Básicamente distinguimos entre dos tipos de operaciones en un archivo : operaciones de lectura, leemos el contenido, y operaciones de escritura, donde escribimos el contenido.

Pascal, además permite abrir el archivos con diferentes finalidades y con diferentes funciones. Si lo queremos abrir de sólo lectura, sin poder realizar escritura, emplearemos la función Reset. Si lo que queremos es reescribir de nuevo el archivo, sin posibilidad de leer, emplearemos Rewrite. Además, los archivos de texto permiten añadir datos al final del archivo si este se abre con la orden Append, la cual es exclusiva de los archivos de texto. Las tres funciones toman como único parámetro el alias del archivo.

Reset(ArchivoTexto); // Sólo podemos leer

// o bien

Rewrite(ArchivoTexto); // Borramos el archivo y sólo podemos escribir

// o bien

Append(ArchivoTexto); // Sólo podemos escribir al final del archivo

La diferencia fundamental entre Rewrite y Append se encuentra en que Rewrite borra todos los datos del archivo si ya existiese, de lo contrario lo crea, mientras que Append crea el archivo si no lo existe, de lo contrario se sita al final del archivo. Reset sólo funciona con archivos que ya existen, en caso contrario genera un error.

El llamado cursor de lectura/escritura se refiere al punto desde el cual empieza nuestra lectura o escritura. Al abrir con Reset o Rewrite el cursor siempre se encuentra al principio del archivo. En el caso de Append el cursor se encuentra en el final del archivo de texto de forma que la única cosa que podemos hacer es escribir del final del fichero en adelante, o sea alargándolo. Al leer o escribir en u n archivo el cursor de lectura/escritura adelanta hacia el fin del fichero tantas posiciones como el tamaño del dato escrito o leído. En caso de lectura, leer más allá del fin del archivo es incorrecto mientras que escribir a partir del fin del fichero hace que éste crezca.

Los únicos datos que podemos escribir en un archivo de texto son efectivamente cadenas de texto. La estructura de los archivos de texto ASCII es bien simple. Constan de líneas formadas por una tira de caracteres que terminan con los caracteres ASCII número 13 y 10 (fin de línea y retorno de carro).

Para leer o escribir una línea desde un archivo de texto emplearemos las funciones Readln o Writeln respectivamente, pero teniendo en cuenta de que la primera variable especificada como parámetro sea el alias del archivo y el segundo parámetro sea un String. Sólo en el caso de Writeln, el segundo parámetro puede ser un literal de String mientras que Readln exige una variable String como segundo parámetro.

Una vez hemos finalizado el trabajo con el archivo conviene cerrarlo para asegurarse de que los datos escritos se escriben correctamente y para indicar que ya no lo vamos a emplear más. La función Close^3 <#sdfootnote3sym>, que toma como parámetro el alias del archivo, se encarga de esta tarea.

Por ejemplo este programa escribe la fecha actual a un archivo de texto :

program EscribirFecha;

uses Dos, SysUtils;

{ La unit DOS se necesita para GetDate la unit SysUtils es necesaria para IntToStr }

const

DiasSemana : array[0..6] of string = ('Domingo', 'Lunes', 'Martes','Miércoles', 'Jueves', 'Viernes', 'Sábado');

var

ArchivoTexto : Text;

Anyo, Mes, Dia, DiaSemana : Integer;

begin

Assign(ArchivoTexto, 'FECHA.TXT');

Rewrite(ArchivoTexto); // Lo abrimos para escritura borrándolo todo.

GetDate(Anyo, Mes, Dia, DiaSemana);

Writeln(ArchivoTexto, DiasSemana[DiaSemana]+' '+IntToStr(Dia)+'/'+IntToStr(Mes)+'/'+IntToStr(Anyo));

Close(ArchivoTexto);

end.

Antes de seguir hay que hacer un comentario sobre el array DiasSemana que hemos empleado. Es posible declarar constantes con tipo de forma que se especifica de qué tipo son y su valor inicial, ya que las constantes con tipos pueden ser modificadas a lo largo del programa, aunque no suele ser muy usual. En el caso de los arrays la sintaxis obliga a separar los literales entre paréntesis. En caso de arrays multidimensionales hay que emplear la misma sintaxis y separarla entre comas :

const

ArrayBi : array [0..1, 0..1] of integer = ((-3, 4), (2, 7));

El array que hemos declarado lo empleamos para saber el nombre del día de la semana. Los valores del día actual se obtienen gracias a la función GetDate a la cual le pasaremos cuatro variables: día, mes año y una para el día de la semana. Mediante el array DiasSemana podemos obtener el nombre de los días de la semana ya que GetDate devuelve un valor entre 0 y 6 dónde cero es domingo y el seis es sábado. De esta forma el día de la semana devuelto por GetDate coincide con los nombres del array.

En el Writeln posterior hemos concatenado el día de la semana, el día, el mes y el año, aunque por ser Writeln podríamos haber separado estos elementos por comas como si fueran más parámetros.

Vamos a leer el archivo que hemos creado con otro programa que lea una línea del archivo y la imprima por la pantalla.

program LeerFecha;

var

ArchivoTexto : Text;

CadenaFecha : string;

begin

Assign(ArchivoTexto, 'FECHA.TXT');

Reset(ArchivoTexto); // Lo abrimos para sólo lectura

Readln(ArchivoTexto, CadenaFecha);

Writeln(CadenaFecha);

Close(CadenaFecha);

end.

Leemos una sola línea del archivo de texto y la almacenamos en la variable CadenaFecha. Después la mostramos en pantalla para comprobar que el dato leído es el que realmente había en el archivo. Finalmente cerramos el archivo.

El uso de Read en vez de Readln no es recomendable ya que puede tener un comportamiento extraño. En cambio, si lo que queremos es añadir una cadena sin saltar de línea podemos emplear Write con la misma sintaxis Writeln.

Hay otras funciones muy interesantes y útiles para los archivos de texto, todas toman como parámetro el alias del archivo de texto. Eof (End of file) devuelve true si el cursor del archivo ha llegado al final de éste. Eoln (End of line) devuelve true si el cursor se encuentra al final de una línea. Esta última función se suele emplear cuando se lee con Read carácter a carácter. La función Eof permite parar la lectura del archivo ya que leer más allá del archivo genera un error de ejecución.

La función Flush descarga los buffers internos del archivo de texto al disco de forma que el buffer, o memoria intermedia, se vacía y se actualiza el archivo.

Las funciones SeekEof y SeekEoln son parecidas a las respectivas Eof y Eoln pero sin tener en cuenta los caracteres en blanco, de forma que si desde la posición del cursor del archivo hasta el fina l del archivo o final de línea todo son blancos SeekEof y SeekEoln, respectivamente, devuelven true.

Truncate suprime todos los datos posteriores a la posición actual del cursor del archivo.

Trabajar con archivos con tipo

Para declarar que un archivo está formado por datos de un sólo tipo hay que declararlo con las palabras reservadas file of y el tipo del archivo :

type

TAgenda = record

Nombre, Apellidos : string;

NumTelefono : Integer;

end;

var

Agenda : file of TAgenda;

De esta forma podríamos implementar una agenda muy rudimentaria con sólo tres campos (Nombre, Apellidos y NumTelefono). Para crear, escribir y leer del archivo el funcionamiento es idéntico a los archivos de texto con la diferencia que hay que leer los datos en una variable del tipo del archivo. También hay pequeñas diferencias respecto a la apertura del archivo. Reset permite la lectura y la escritura, en archivos de texto sólo permitía lectura. Append no se puede emplear en archivos que no sean de texto y Rewrite se comporta igual que con los archivos de texto.

Vamos a ver un ejemplo de como podemos añadir datos al archivo de agenda :

program EscribirArchivoTipo;

uses SysUtils; // Para la función FileExists

const

NombreArchivo = 'AGENDA.DAT';

type

TAgenda = record

Nombre, Apellidos : string;

NumTelefono : Cardinal;

end;

var

Agenda : file of TAgenda;

DatoTemporal : TAgenda;

NumVeces, i : Integer;

begin

Assign(Agenda, NombreArchivo); // Asignamos el archivo

if not FileExists(NombreArchivo) then

begin

Rewrite(Agenda); // Si no existe lo creamos

end

else

begin

Reset(Agenda); // Lo abrimos para lectura escritura

end;

Write('Cuantas entradas desea añadir en la agenda ? ');

Readln(NumVeces);

for i := 1 to NumVeces do

begin

Write('Introduzca el nombre : '); Readln(DatoTemporal.Nombre);

Write('Introduzca los apellidos : '); Readln(DatoTemporal.Apellidos);

Write('Introduzca el teléfono : '); Readln(DadaTemporal.NumTelefono);

Write(Agenda, DatoTemporal); // Escribimos DatoTemporal al archivo

Writeln; // Equivale a Writeln(); hace un salto de línea

end;

Close(Agenda); // Cerramos la agenda

end.

Observamos en el código de que cuando el archivo no existe entonces lo creamos automáticamente con la función Rewrite. En caso contrario lo podemos abrir con Reset. La función FileExists permite saber si el archivo especificado existe ya que devuelve true en caso afirmativo.

Pedimos al usuario que nos diga cuantas fichas desea introducir y solicitamos que introduzca los datos. Después almacenamos la variable DatoTemporal al archivo agenda con una llamada a Write. No es posible emplear Writeln en archivos con tipo. Cuando llamamos a Writeln sin parámetros en pantalla se ve un salto de línea. Lo empleamos para distinguir los diferentes registros que vamos introduciendo al archivo.

Un archivo con tipo se entiende que está formado por un numero determinado de datos del tipo del archivo, en nuestro caso son registros de tipo TAgenda.

Para saber el número de registros que tiene nuestro archivo emplearemos la función FileSize como parámetro el alias del archivo. El resultado que nos devuelve esta función es el nombre de registros del archivo. Emplearemos esta función en el ejemplo siguiente de lectura del archivo.

program LeerArchivoTipo;

uses SysUtils; // Para la función FileExists

const

NombreArchivo = 'AGENDA.DAT';

type

TAgenda = record

Nombre, Apellidos : Shortstring;

NumTelefono : Cardinal;

end;

var

Agenda : file of TAgenda;

DatoTemporal : TAgenda;

i : Integer;

begin

Assign(Agenda, NombreArchivo); // Asignamos el archivo

if not FileExists(NombreArchivo) then

begin

Writeln('El archivo ', NombreArchivo, ' no existe.');

end

else

begin

Reset(Agenda); // Lo abrimos para lectura y escritura

for i := 1 to FileSize(Agenda) do

begin

Read(Agenda, DatoTemporal);

Writeln('Nom : ', DatoTemporal.Nombre);

Writeln('Cognoms : ', DatoTemporal.Apellidos);

Writeln('Teléfon : ', DatoTemporal.NumTelefono);

Writeln;

end;

Close(Agenda);

end;

end.

En este caso, vamos leyendo los registros guardando los valores a la variable temporal DatoTemporal. Después escribimos esta información en la pantalla. Siempre que el archivo exista, claro.

Tal como hemos comentado antes, los archivos con tipo son archivos de acceso aleatorio y podemos escribir y leer datos en el orden que queramos. Para poder acceder a una posición determinada del archivo hay que emplear la función Seek.

Esta función necesita dos parámetros : el primero es el alias del archivo y el segundo el número del elemento al cual queremos acceder. Hay que tener en cuenta que el primer elemento es el cero y el último es el valor de FileSize menos 1.

Por ejemplo, para leer el archivo en sentido inverso al habitual, puesto que Read adelanta el cursor de lectura automáticamente que vamos leyendo, necesitaremos la función Seek.

program LeerAlReves;

// Aquí van las declaraciones del ejemplo anterior : TAgenda, Agenda,

begin

Assign(Agenda, NombreArchivo); // Asignamos el archivo

if not FileExists(NombreArchivo) then

begin

Writeln('El archivo', NombreArchivo, ' no existe.');

end

else

begin

Reset(Agenda); // Lo abrimos para lectura y escritura

for i := FileSize(Agenda)-1 *downto* 0 do

begin

Seek(Agenda, i); // Nos movemos al punto especificado por i

Read(Agenda, DatoTemporal);

Writeln('Nombre : ', DatoTemporal.Nombre);

Writeln('Apellidos : ', DatoTemporal.Apellidos);

Writeln('Teléfono : ', DatoTemporal.NumTelefono);

Writeln;

end;

Close(Agenda);

end;

end.

Si en algún momento queremos saber la posición del cursor del archivo tenemos que emplear la función FilePos y el alias del archivo. Esta función devuelve un entero que, a diferencia de Seek, puede estar comprendido entre 1 y el valor de FileSize.

Archivos sin tipo

Los archivos sin tipo no tienen ningún formato especial y por tanto podemos leer de ellos los datos que queramos. Hay que tener en cuenta, que en ninguno de los tres tipos de archivos se hace ningún tipo de comprobación de los datos que se escriben o se leen. De forma que si abrimos un archivo de un tipo determinado, por ejemplo de tipo Longint, con un alias de tipo Byte los datos que leamos corresponderán a los bytes del Longint.

Las funciones Write y Read no se pueden emplear con archivos sin tipo. Es por este motivo que tenemos que emplear las funciones BlockWrite y BlockRead. Estas funciones leen o escriben un numero n, o menos en caso de lectura, de bloques de tamaño b, en bytes, que conviene especificar en las funciones Rewrite y Reset. Si no especificamos el valor del bloque a Rewrite o Reset por defecto vale 128 bytes.

Como ejemplo, implementaremos dos programas. En el primero escribiremos en un archivo un string de 35 caracteres y después un entero. En el segundo nos limitaremos a recoger estos datos y a escribirlos en pantalla.

program EscribirArchivoSinTipo;

const

NombreArchivo = 'DATOS.DAT';

var

Datos : file;

Nombre : string[35];

Edad : Integer;

begin

Assign(Datos, NombreArchivo); // Asignamos el archivo

Rewrite(Datos, 1); { Especificamos 1 byte como tamaño del bloque por comodidad }

Write('Introduzca un nombre : '); Readln(Nombre);

Write('Introduzca una edad : '); Readln(Edad);

BlockWrite(Datos, Nombre, SizeOf(Nombre));

BlockWrite(Datos, Edad, SizeOf(Edad));

Close(Datos);

end.

Obsérvese, que especificamos el tamaño del registro en 1 ya que después necesitaremos especificar el tamaño de los datos que vamos a escribir. Para esto emplearemos la función SizeOf que toma como parámetro cualquier tipo de variable y devuelve su tamaño en bytes. Por tanto Edad tiene un tamaño de 2 bytes y el tamaño de Nombre es de 36 bytes. Este es el valor que se pasa como tercer parámetro y que entiende que queremos copiar 36 bloques de un byte y después 2 bloques de un byte. Si no hubiéramos especificado el tamaño del bloque, se tomaría por defecto 128 bytes de forma que al escribir el entero coparíamos 2 bloques de 128 bytes (o sea 256 bytes). La función BlockWrite admite cualquier tipo de variable como segundo parámetro.

Para leer, la función BlockRead funciona de forma parecida a BlockWrite. En este caso el segundo parámetro también puede ser de cualquier tipo, pero no una constante.

program LeerArchivoSinTipo;

const

NombreArchivo = 'DATOS.DAT';

var

Datos : file;

Nombre : string[35];

Edad : Integer;

begin

Assign(Datos, NombreArchivo); // Asignamos el archivo

Reset(Datos, 1); { Especificamos el tamaño del bloque de 1 byte por comodidad }

BlockRead(Datos, Nombre, SizeOf(Nombre));

BlockRead(Datos, Edad, SizeOf(Edad));

Writeln(Nombre, ' tiene ', Edad, ' años');

Close(Datos);

end.

Otras funciones que podemos emplear para los archivos sin tipo incluyen : FileSize, FilePos y Seek. Todas tres funciones trabajan con el valor del bloque, el tamaño del cual hemos especificado en Reset o Rewrite. También podemos emplear Eof para detectar el fin del archivo.

Gestión de archivos

A diferencia de crear archivos, escribir o leer datos también podemos realizar otras operaciones como pueden ser eliminar un archivo o cambiarle el nombre.

Eliminar un archivo

Eliminar un archivo es tan simple como emplear la función Erase y el alias del archivo. Hay que tener en cuenta de que el archivo tiene que estar cerrado para evitar un error en tiempo de ejecución. Además, cerrar un archivo ya cerrado da error de ejecución. Para asegurarse de que un archivo está cerrado podemos abrirlo con Reset y cerrarlo con Close. Nuevas llamadas a Reset con un archivo ya abierto no dan error, simplemente sitúan el cursor del archivo al inicio. Una vez está cerrado ya podemos llamar a Erase.

Renombrar un archivo

La función Rename permite renombrar un archivo. Hay que tener en cuenta de que el archivo también tiene que estar cerrado.

var

archivo : file;

begin

Assign(archivo, 'NombreViejo.dat'); // Suponemos que existe

Rename(archivo, 'NombreNuevo.dat');

end.

Capturar errores de archivos con IOResult

Hasta ahora hemos operado con archivos suponiendo que el archivo existía o bien no había ningún error durante las operaciones de lectura o de escritura.

Existe una forma muy sencilla de capturar los errores de las operaciones con archivos mediante la variable IOResult y la directiva de compilador {$I}

Una directiva de compilador es un comentario que justo después de la llave hay el símbolo de dólar $ y una o más letras. Las directivas modifican el comportamiento del compilador en tiempo de compilación. Después de una directiva el compilador puede modificar su comportamiento de muchas formas. Muchas veces van asociadas a la llamada compilación condicional, que veremos más adelante, en otras se refiere a como se estructuran los datos, etc.

En el caso de $I es una directiva de activación/desactivación. Por defecto $I está activada con lo cual los errores de entrada/salida de archivos provocan un error de ejecución (runtime error). Los errores runtime son irrecuperables, o sea, provocan la finalización inmediata y anormal del programa. Muy a menudo, el error puede ser debido al usuario con lo cual hay que procurar de no genera un error runtime ya que inmediatamente terminaría el programa. Podemos evitar estos errores con la desactivación de la directiva $I. Para activar una directiva de activación/desactivación (llamada switch) tenemos que incluir la siguiente directiva en medio del código.

{$I+}

Para desactivarla :

{$I-}

Es importante reactivar la directiva $I cuando ya no sea necesaria. Cuando está desactivada podemos acceder a la variable IOResult. Esta variable tiene el valor cero si la operación con archivos se ha llevado a cabo con éxito. En caso contrario, almacenará un valor distinto de cero. Vamos a aplicar esta técnica al programa de cambio de nombre :

var

archivo : file;

begin

Assign(archivo, 'NombreViejo.dat'); // Assign no da nunca error

{$I-} // Desactivamos la directiva $I

Rename(archivo, 'NombreNuevo.dat');

if IOResult <> 0 then

begin Writeln('Este archivo no existe o no se ha podido renombrar');

end;

{$I+} // Importante reactivar la directiva cuando ya no sea necesaria

end.

Así de simple es capturar los errores de lectura/escritura. Suele ser habitual, por ejemplo, emplearlo en la función Reset ya que si el archivo no existe se produce un error que se notifica en IOResult.

Es posible referirse a la directiva $I con su nombre largo $IOCHECKS. Además FreePascal permite la activación y desactivación mediante on y OFF en los nombres largos de directivas.

{$I+} // Activar

{$I-} // Desactivar

{$IOCHECKS on} // Activar, cuidado el espacio

{$IOCHECKS OFF} // Desactivar

{$IOCHECKS +} // Activar, cuidado el espacio

{$IOCHECKS -} // Desactivar

Hay más directivas de compilador que veremos más adelante.

Funciones y procedimientos

Diseño descendiente

Se llama diseño descendiente el método de diseño de algoritmos que se basa en la filosofía de "divide y vencerás". Donde el programa se divide en partes más pequeñas que se van desarrollando con una cierta independencia respecto a las otras partes del programa.

De esta forma podemos definir una especie de subprogramas o subrutinas para ser ejecutada diversas veces dentro del programa en si. Los problemas pueden ser descompuestos en otros problemas más pequeños pero también más fáciles de resolver. Esto permite, también, aprovechar mejor el código y permite modificarlo más cómodamente pues no habrá que modificar todo el programa, sólo el código del subprograma.

En Pascal el diseño descendiente se lleva a cabo con dos elementos muy importantes : las funciones y los procedimientos.

Funciones vs Procedimientos

La diferencia más importante entre un procedimiento y una función es el hecho de que una función es una expresión y como a tal se puede emplear dentro de otras expresiones. Un procedimiento, en cambio, representa una instrucción y no puede ser empleada dentro de un contexto de expresión.

Las funciones devuelven siempre un valor de un tipo determinado. Ya hemos visto el caso de la función FileExists de la unit SysUtils. Esta función devuelve un tipo booleano por lo que podíamos incluirla dentro de una expresión booleana como esta :

if not FileExists(NombreArchivo) then

La función Write es, por ejemplo, un procedimiento y por tanto no es sintácticamente legal incluirla dentro de una expresión.

Aun así, la sintaxis de Pascal se ha relajado a lo largo del tiempo y es posible emplear una función como si de un procedimiento se tratara. En este caso se pierde el valor que devuelve la función aunque se ejecuta igualmente.

Por ejemplo, si queremos que un programa se pare hasta que el usuario pulse una tecla podemos emplear la función Readkey de la forma siguiente :

Readkey; // El programa se parará hasta que el usuario pulse una tecla

El carácter que devuelve ReadKey, la tecla que ha pulsado el usuario, se perderá a menos que incluyamos ReadKey en una expresión.

Contexto de las funciones y los procedimientos

Las funciones y los procedimientos tienen que declararse siempre antes del módulo principal del programa. Si una función, o procedimiento, se emplea en otra ésta tendrá que estar declarada antes o bien encontrarse en una unit de la clausula uses.

Funciones

Tanto las funciones como los procedimientos tienen que tener un nombre que las identifique, que seguirá las reglas de los identificadores de Pascal, y opcionalmente pueden tener un conjunto de parámetros que modifique de alguna forma el comportamiento de la función.

La declaración de una función sigue el modelo siguiente :

function NombreFuncion ( listaParametros ) : tipoRetorno;

NombreFuncion es el identificador de la función y es el nombre que emplearemos para llamarla. ListaParametros representa el conjunto de parámetros que se pueden pasar a la función. Para declarar más de un parámetro los tenemos que separar entre puntos y comas y teniendo en cuenta de que el último parámetro no lleva nunca punto y coma. Estos parámetros reciben el nombre de parámetros formales y es importante distinguirlos de parámetros reales que representan los parámetros con los que se ha llamado la función. Finalmente, tipoRetorno representa el tipo de datos que devuelve la función.

Vamos a definir una función muy sencilla max que devuelva el valor del máximo de dos parámetros enteros a y b.

function max(a : integer; b : integer) : integer;

{ Podíamos haber escrito los parámetros así : (a, b : integer) }

begin

if a >= b then

begin

max := a

end

else

begin

max := b;

end;

end;

Obsérvese con detenimiento el código de la función max. Para empezar, las sentencias van entre un begin y un end con punto y coma ya que no es el bloque principal del programa. Nótese también que para especificar el valor que tiene que devolver la función hacemos una asignación al identificador de la función, max.

Como ejemplo de empleo de esta función, haremos un programa que pida dos enteros al usuario e indique cual de los dos es máximo.

program Funciones;

var

n, m : integer;

function max(a, b : integer) : integer;

begin

if a >= b then

begin

max := a

end

else

begin

max := b;

end;

end;

begin

Write('Introduzca el valor entero M : '); readln(m);

Write('Introduzca el valor entero N : '); readln(n);

Writeln('El máximo entre ', M, ' y ', N, ' es ', max(m, n));

end.

Como se puede ver en la última línea del código, a diferencia de la declaración y tal como hemos llamado hasta ahora las funciones o procedimientos con más de un parámetro, hay que separar los distintos parámetros entre comas. Hay que tener en cuenta que los parámetros reales m y n del ejemplo no tienen nada que ver con los parámetros formales de max (a y b). De hecho los parámetros formales sirven sólo para implementar la función e indicar el orden de estos. No hay que decir que en vez de una variable podemos emplear un literal.

Vamos a definir una nueva función min a partir de max.

function min(a, b : integer);

begin

min := -max(-a, -b);

end;

Esta implementación se basa en que los números negativos se ordenan al revés que los positivos, por lo que cambiando dos veces el signo es suficiente.

Es importante siempre incluir una asignación al valor de retorno de la función ya que en caso contrario el valor de la función podría quedar indeterminado. Una forma segura es incluir la asignación al final de la función, si es posible, o asegurarse de que en todos los casos posibles de salida hay al menos una asignación de este tipo.

Procedimientos

Los procedimientos se declaran de forma parecida a las funciones pero sin especificar el tipo de salida ya que no tienen. Por ejemplo el procedimiento Creditos escribiría unos créditos en la pantalla :

procedure Creditos;

begin

Writeln('(C) Manual de FreePascal 1.00'

Writeln('Roger Ferrer Ibáñez – 2001');

end;

En vez de emplear la palabra reservada function emplearemos procedure. Tampoco hay que realizar ninguna asignación al identificador del procedimiento pues los procedimientos no devuelven nada. Tanto en funciones como en procedimientos, en caso de que no tengamos parámetros no habrá que poner los paréntesis. En el caso de las funciones antes del punto y coma tendrá que ir el tipo de retorno.

Como ejemplo tenemos la definición de ReadKey es :

function Readkey : Char;

En el momento de llamar a una función o procedimiento sin parámetros podemos omitir los paréntesis de parámetros o, si los queremos escribir, sin nada en medio (sin contar los caracteres en blanco).

Creditos; // Ambas llamadas son correctas

Creditos(); // Esta llamada tiene una sintaxis más parecida a C

Tipos de parámetros

Hasta ahora hemos visto que una función puede devolver un valor y que por tanto la podemos incluir dentro de una expresión. Muchas veces nos puede interesar especificar parámetros que impliquen un cambio en estos parámetros y esto no es posible de implementar en una simple función.

Supongamos un procedimientos en el cual pasando dos parámetros de tipo entero queremos que se intercambien los valores. Una forma de hacerlo es mediante sumas y restas. Nosotros implementaremos un caso más general mediante el uso de una tercera variable temporal.

procedure Intercambiar(a, b : integer);

var

temporal : integer;

begin

temporal := b;

b := a;

a := temporal;

end;

Si escribimos un programa para verificar si el procedimiento funciona, nos daremos cuenta de que los parámetros no se han modificado después de la llamada. O sea, no ha habido intercambio.

a := 10;

b := 24;

Intercambiar(a, b);

// Ahora a aún vale 10 y b aún vale 24 !!!

Esto es porque hemos empleado un tipo de parámetro llamado parámetro por valor. Los parámetros por valor no son nada más que una copia de los parámetros originales que se crean sólo para la función o procedimiento. Una vez termina esta copia desaparece, por lo que toda modificación que hagamos dentro de la función o procedimiento se perderá.

Para resolver este problema podemos emplear parámetros por referencia. En este caso la función o procedimiento conoce exactamente la posición de memoria del parámetro por lo que las modificaciones que hagamos dentro de la función o procedimiento permanecerán después de la llamada. Para indicar que un parámetro es por referencia incluiremos la palabra reservada var delante del parámetro. El procedimiento intercambiar quedaría así :

procedure Intercambiar(var a : integer; var b : integer);

{ Podíamos haber escrito como parámetros (var a, b : integer); }

var

temporal : integer;

begin

temporal := b;

b := a;

a := temporal;

end;

Una de las ventajas de los parámetros por referencia es que no es necesario crear una copia cada vez que se llama la función o procedimiento, con lo cual ahorramos algo de tiempo sobretodo si el dato es grande (como arrays muy extensos o registros con muchos campos). El riesgo, pero, es que cualquier modificación (por accidental que sea) permanecerá después de la llamada. Para minimizar este riesgo disponemos de un tercer tipo de parámetro llamado parámetro constante. En vez de var llevan la palabra reservada const. El compilador no permitirá ningún tipo de modificación fortuita de los datos pero los parámetros se pasaran por referencia y por tanto la función o procedimiento será más eficiente.

En el caso anterior intentar compilar procedure Intercambiar(const a, b : integer); daría un error de compilación pues entendería a y b como constantes.

Es importante remarcar que en caso de pasar variables por referencia no es posible especificar literales o constantes en este tipo de parámetros ya que si sufrieran alguna modificación dentro de la función o procedimiento no se podría almacenar en ningún sitio. Además, el compilador es más estricto a la hora de evaluar la compatibilidad del tipo. Por ejemplo si el parámetro por referencia es de tipo Integer y pasamos una variable Byte el compilador lo considerará ilegal aunque ambos sean enteros y, en general, un Byte es compatible con un Integer.

En el caso de parámetros constantes, el compilador no es tan estricto y permite pasar literales y constantes.

Variables locales y globales

En el ejemplo anterior de intercambiar hemos definido una variable temporal de tipo entero dentro del cuerpo del procedimiento. Estas variables declaradas dentro de una función o procedimiento reciben el nombre de variables locales y sólo son accesibles dentro de las funciones o procedimientos. Por lo que, intentar asignar un valor a temporal fuera de intercambiar es sintácticamente incorrecto ya que no es visible en este ámbito.

Las variables declaradas al principio del programa reciben el nombre de variables globales. Estas variables se pueden emplear tanto en funciones y procedimientos como en el bloque principal.

También el bloque principal puede tener sus propias variables locales si las declaramos justo antes de éste.

Esta consideración para variables es extensiva a tipos y constantes.

program Variables;

var

a : integer;

procedure Asignar;

var

b, c : integer;

begin

a := 15; // CORRECTO a es una variable global

b := 10; // CORRECTO b es una variable local

c := b; // CORRECTO c es una variable local

d := b; { INCORRECTO d es una variable del bloque principal que no se puede acceder desde aquí }

end;

var

b, d : integer;

begin

a := 10; // CORRECTO a es una variable global

b := 5; // CORRECTO b es una variable local del bloque principal

Asignar; // ATENCI"N!! b no cambiará su valor después de la llamada (b=5)

c := 4; { INCORRECTO!! c no es accesible desde aquí ya que es una variable local de Asignar }

end.

A banda de los diferentes errores sintácticos hay que remarcar la diferencia entre b del procedimiento Asignar y b del bloque principal. Ambas variables son totalmente independientes entre ellas. Este es el único caso en que es posible la existencia de dos variables con nombres iguales. Si la variable b hubiera sido declarada global, juntamente con a, el compilador no nos hubiera permitido declarar b en ningún otro punto pues habría una redeclaración de identificador y esto no es posible pues el compilador no sabría como distinguir las variables. Veremos más adelante, que en diferentes units se pueden declarar los mismos identificadores pues se puede especificar que identificador queremos.

Partes: 1, 2, 3, 4, 5, 6
 Página anterior Volver al principio del trabajoPágina siguiente