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