Técnicas de Programación Apuntes de Clases
Prof. Miguel Guanira.
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
PREFACIO ................................................................. ............................................................................... .............. 4 CAPÍTULO 1: Almacenamiento de datos en el computador ......................... 6
Memoria del computador computad or ............................................................................................................................... ............................................................................................................................... 6 Memoria Principal Principa l............................................................................................................................................. 6 Memoria Secundaria Secundar ia ....................................................................................................................................... 7 Codificación de la información para su almacenamiento ...................................................................... 7 Representación de números negativos ...................................................................................................... 9 Representación de números reales o de punto flotante .................................................................... 12 Almacenamiento Almacena miento invertido invert ido o “back-words” “back- words” ............................................................................................. 15 Representación Represe ntación de caracter c aracteres es ................................................................................................................... 16 Representación de cadenas de caracteres ............................................................................................ 18 Otras representacion repres entaciones es más complejas .................................................................................................. 19
CAPÍTULO 2: Software Soft ware y lenguajes leng uajes de programación program ación .............................. 24
Conceptos generales general es de software softwar e............................................................................................................ 24 Clasificación Clasifica ción del software softwar e.......................................................................................................................... 24 Sistema Operativo Operati vo........................................................................................................................................ 24 Algoritmos Algori tmos ....................................................................................................................................................... 26 Lenguajes de programación program ación........................................................................................................................ 29
CAPÍTULO 3: Ciclo de vida de un proceso de desarrollo de software ............ ............ 34 Pasos involucrados en la solución de problemas en computación .................................................... 34 Traductores de lenguajes de programación: ........................................................................................ 39 Compiladores e Intérpretes ...................................................................................................................... 39
CAPÍTULO 4: Estructura general de un programa ................................. 41
Definición Definici ón de programa program a ................................................................................................................................ 41 Concepto de Identificador ......................................................................................................................... 41 Partes constitutivas de un programa: ..................................................................................................... 42 Asignación Asignaci ón de valores y expresiones expr esiones ....................................................................................................... 49 Instrucciones que permiten la salida y el ingreso de datos............................................................. 66 Ejemplos de programas pr ogramas secuenciales: secuencia les: ..................................................................................................... 72 Introducción al uso de archivos de texto ............................................................................................. 80 Ejemplos de programas que emplean archivos de textos: ................................................................ 84
CAPÍTULO 5: Programac Pr ogramación ión estructurada estructura da ........................................... 89
Estructura Estru ctura Secuencial Secuenci al .................................................................................................................................. 91 Estructura Estru ctura Selectiva Selecti va .................................................................................................................................... 91 Estructura Iterativa.................................................................................................................................... 97 Otras Estructuras Estruc turas ...................................................................................................................................... 105 Solución de problemas con estructuras de control .......................................................................... 109
CAPÍTULO 6: Programación Programa ción modular: ............................................... 120
Implementación de la programación modular ...................................................................................... 121 Variables globales, locales y estáticas ................................................................................................. 125 Parámetros por valor y por referencia ................................................................................................ 130 Solución de problemas empleando diseño descendente y programación pr ogramación modular ...... ............ ............ ........ 135
CAPÍTULO 7: Aplicaciones Aplicacion es con arreglos arreglo s ............................................ 149
Definición Definici ón ....................................................................................................................................................... 151 Implementación de arreglos unidimensionales .................................................................................... 151 Lectura e impresión de los datos en un arreglo unidimensional .................................................... 154 2
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Aplicaciones que emplean arreglos de una dimensión ...................................................................... 156 Ordenación Ordenac ión de datos ................................................................................................................................... 171 Cómo desordenar datos.............................................................................................................................. 181
CAPÍTULO 8: Arreglos Arreglo s de dos o más dimensiones .................................187 Implementación de arreglos de más de una dimensión .................................................................... 187 Aplicaciones que emplean arreglos de dos o más dimensiones ...................................................... 189
CAPÍTULO 9: Manejo de cadenas de caracteres caracter es ..................................211 Definición Definici ón ....................................................................................................................................................... 211 Declaración de una cadena de caracteres ........................................................................................... 212 Funciones y procedimientos que manejan cadenas: .......................................................................... 213 Aplicaciones que emplean cadenas de caracteres: ........................................................................... 223
CAPÍTULO 10: Registros Registro s ........................................................... .............................................................239
Definición Definici ón ...................................................................................................................................................... 239 Implementación Implement ación de un registro r egistro ............................................................................................................... 239 Situaciones complejas en la implementación de un registro .......................................................... 242 Aplicaciones que emplean registros ...................................................................................................... 244
CAPÍTULO 11: Archivos binarios ................................................... ....................................... ............255
Definición Definici ón ...................................................................................................................................................... 255 Formas en las que se puede almacenar información en un archivo .............................................. 255 Diferencias funcionales entre un archivo de textos y uno binario .............................................. 256 Funciones y procedimientos elementales que manejan archivos binarios:................................. binarios: ................................. 260 Acceso secuencial a un archivo binario ................................................................................................ 263 Acceso directo direct o a un archivo ar chivo binario ...................................................................................................... 268 Aplicaciones que emplean archivos binarios.................................... ¡Error! Marcador no definido.
3
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
PREFACIO Durante los años que tengo enseñando a programar a alumnos a lumnos de pregrado, he notado que la mayoría de libros que se emplean para esta tarea están orientados más a la enseñanza de un lenguaje de programación. Estos volúmenes presentan tan solo reglas de sintaxis que muestran cómo escribir determinada instrucción, y con ejemplos muy sencillos muestran cómo estas instrucciones se deben colocar en un programa. Sin embargo, aprender a programar un computador es una tarea más compleja que aprender un conjunto de reglas sintácticas; crear un programa implica primero, haber entendido el problema que se nos presenta, luego encontrar la manera de solucionar el problema, que en la mayoría de casos esta solución estará enfocada al problema mismo y no a la forma cómo trabaja el computador ni al lenguaje de programación que se vaya a utilizar. Una vez que se encontró la solución se deben ordenar las ideas que implican esta solución describiéndola paso a paso. Concluida esta tarea, recién se empieza a plasmar la solución en un programa de computador. Es por este motivo que se concibió la idea de crear un texto que se enfoque más a la enseñanza de la programación en lugar que a la de aprender un lenguaje de programación. El primer capítulo estará centrado a la forma cómo se almacenan y representan los datos en el computador, esto ayudará a tomar conciencia de las limitaciones que tiene el computador de modo que a la hora de escribir el programa se tomen en cuenta para conseguir la solución esperada. En el segundo capítulo se hará una revisión general de los conceptos relacionados con el software y los lenguajes de programación. En capítulo tres veremos en modo general qué pasos se deben seguir para solucionar un problema. A partir de allí el texto se enfocará en el planteamiento de problemas y de cómo llegar a solucionarlo, incluyendo en cada uno los conceptos de programación requeridos. Se empezará con problemas sencillos para ir aumentando paulatinamente la dificultad hasta llegar a plantear problemas que requieran estructuras de datos complejas como arreglos, registros, etc. La meta de este texto es la de aprender a desarrollar programas, se puede afirmar que no se aprende a programar en forma teórica, la solución de un problema debe concluirse con la escritura del programa, su colocación en el computador y su ejecución para ver si la respuesta que da se ajusta a la esperada para la solución del problema. Por esto el texto estará ligado a un lenguaje de programación, al principio, como en el capítulo cuatro, se mencionarán varios lenguajes de programación, pero conforme se aumente la complejidad del los problemas se centrará en uno solo. El lenguaje de programación que se ha elegido para este fin es el Pascal y la razón para esto es que Pascal fue diseñado para aprender conceptos de programación, otros lenguajes como el C, C++, Java etc. han sido diseñados para producir programas, alguien que ya aprendió a programar encontrará un deleite en emplear estos últimos lenguajes porque incluyen elementos que facilitan la presentación final del programa o que solucionan, a modo de “cajas negras”, partes complejas de la solución; sin embargo para una persona que recién está aprendiendo a programar, estos elementos no hacen más que terminar confundiéndolos o distrayéndolos en puntos de la programación que no darán solución al problema que se plantea. 4
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Por el contrario el lenguaje Pascal es un lenguaje muy sencillo y fácil de aprender, además definen reglas muy estrictas que hacen que el programador inexperto se de cuenta muy rápidamente de sus errores al momento de compilar su programa, reforzando por lo tanto los conceptos aprendidos. Además el lenguaje es muy rico en instrucciones y en la posibilidad de emplear estructuras de datos muy complejas lo que favorece la elaboración de programas más complicados. Una vez que se haya aprendido los conceptos de programación y adquirido una cierta práctica con este lenguaje, la migración a otros lenguajes de programación más modernos y complejos se hará con un mínimo de esfuerzo.
5
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 1: Almacenamiento de datos en el computador Una de las primeras cosas que se debe tener en cuenta para aprender a programar un computador es la forma cómo se almacena la información en la computadora. Cuando una persona ajena al mundo de la computación se pone delante de la máquina, ve un apantalla que contiene muchas imágenes, íconos, etc., y que por medio de una serie de dispositivos como el teclado, el mouse, o la misma pantalla, puede crear documentos, trazar una serie de gráficos, crear imágenes o hacer cálculos complejos. Aquellas personas, que simplemente usan el computador, no se dan cuenta lo que hay detrás de esa máquina, me refiero a la manera cómo ese texto, esa imagen o ese sonido puede estar almacenado y puede ser procesado por el computador de manera que podamos apreciarlos y sacarle el provecho que le sacamos cada día. La programación de computadores implica la creación de aplicaciones que permita a las personas poder procesar información, esto es, crear aplicaciones para poder escribir texto, almacenarlos, recuperarlos y actualizarlos, poder trazar una gráfica, procesar imágenes, o hacer que se realicen cálculos complejos y reiterativos. Pues bien, antes de poder crear una aplicación lo primero que debemos conocer es cómo se almacena la información en el computador, también se tiene que entender que el computador es una máquina creada por el hombre y por lo tanto un elemento con muchas limitaciones, que un computador no piensa, que sólo se limita a repetir una serie de acciones y que esas acciones no son otra cosa que órdenes conglomeradas en lo que se conoce como un programa o una aplicación de computador. En este capítulo se estudiarán en primer lugar los dispositivos que emplea el computador para almacenar la información, y en segundo lugar, donde nos explayaremos más, veremos cómo se almacena la información en la computadora, veremos cómo una máquina llena de cables y circuitos electrónicos puede guardar y procesar un número, un texto, una imagen, etc. y lo más importante, se las limitaciones que existen y cómo se hace para adaptarse a ellas. Comencemos entonces analizando los medios que tiene le computador para almacenar información.
Memoria del computador El dispositivo que permite almacenar información, vale decir, textos, números, imágenes, sonidos, en la computadora se denomina “memoria del computador”. En un computador podemos distinguir dos tipos de memoria, la memoria principal y la secundaria.
Memoria Principal La memoria principal del computador es el lugar donde el computador colocará la información que está procesando en ese momento. Si se quiere ejecutar un programa como una hoja de cálculo, un procesador de palabras, un manejador de imágenes, etc., el computador colocará primero el programa que se va a ejecutar en la memoria principal y lo ejecutará desde allí. Una vez que el programa esté en ejecución se podrá añadir información al computador, esto es, si estamos trabajando con un procesador de palabras se agregará el texto que se desea crear, este texto también es colocado en la memoria principal del computador. 6
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
La memoria principal tiene un problema, es “volátil”, esto quiere decir que si el computador está desconectado la información que guardaba allí se pierde y no habrá forma de recuperarla. Es por esto que el computador requiere de otro dispositivo que permita almacenar información de modo que ésta no se pierda.
Memoria Secundaria La memoria secundaria sirve para almacenar, de modo permanente, información procesada por el computador. Esto quiere decir que lo que se almacena en este dispositivo, no se pierde así el computador esté apagado, como sucede con el contenido de la memoria principal. El problema con la memoria secundaria es que la información que se guarda allí no se puede procesar, sólo se almacena; para poder procesar la información que se encuentra allí el computador tendrá que llevarla a la memoria principal. Esto último se refiere a que, una imagen (por ejemplo un fotografía), un directorio telefónico, un programa, etc., pueden ser almacenados en la memoria secundaria, sin embargo para que una imagen pueda ser vista en el monitor del computador o impresa en papel, ésta debe ser llevada a la memoria principal del computador. Si, dado el nombre de una persona deseamos obtener su número telefónico, el directorio, parcial o totalmente, debe ser llevado a la memoria principal para realizar la búsqueda. Si deseamos escribir un documento, primero debe ejecutarse el programa Word, para esto se debe llevar el programa a la memoria principal para su ejecución. Una vez escrito el documento, si se desea preservarlo para utilizarlo en otro momento, éste debe ser almacenado en la memoria secundaria. Existen diferentes dispositivos que cumplen la función de memoria secundaria. Estos dispositivos son por ejemplo los discos duros, los discos compactos, las memorias flash o memorias USB, etc.
Codificación de la información para su almacenamiento ¿Cómo hace el computador para almacenar “un texto”, “un número”, “una fotografía”, “un programa”, etc.?, ¿Cómo hace para ejecutar un programa? Esta tarea no es sencilla y se ha requerido una solución muy compleja e ingeniosa. Para entender mejor su funcionamiento, imaginemos un interruptor eléctrico como se muestra en la siguiente figura: Estado 0 (no pasa corriente), o simplemente 0 Estado 1 (pasa corriente), o simplemente 1
Este dispositivo puede estar colocado en dos estados, uno en el que la compuerta está abierta, por lo tanto no pasa la corriente y por lo tanto no es registrada por un detector y otro en el que la compuerta está cerrada y por lo tanto pasa la corriente y esta es detectada. Llamemos al primer caso “estado cero” y al segundo “estado uno”.
7
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Con este dispositivo se podría representar, al detectarse la corriente, de dos números, el cero (con el estado cero) y el uno (con estado uno), coincidentemente estos dos números o dígitos corresponden a los dígitos que se emplean para representar el sistema de numeración binario. A cada uno de estos estados se les denomina “bit” que deriva de la frase en inglés “binary digit”. En los computadores actuales, no existen estos interruptores eléctricos, en lugar de eso existen dispositivos electromagnéticos que permiten representar de estos dos estados y por lo tanto los dos dígitos binarios. Siguiendo con la analogía, con un solo interruptor sólo se puede representar el 0 y el 1, ¿Qué pasa con los demás números p. e.: 23, 1623, 45.67, -12 -89.432, 0.000034? ¿Qué pasa con los textos, las imágenes, el sonido, los videos, etc.? ¿Cómo se almacenan o representan? La respuesta a estas preguntas requiere también soluciones complejas e ingeniosas. Si tuviéramos un dispositivo en el que se agruparan varios de estos interruptores, por ejemplo 8, se tendría un esquema como el que se muestra a continuación:
Cada interruptor podría encontrarse en un estado 0 ó 1. En la imagen anterior, todos están en 0, por lo tanto en conjunto el dispositivo pondrían estar representando el estado 0000 0000, o simplemente el estado 0. Si se tuviera un esquema como el que se muestra a continuación, en el que el primer interruptor estuviera cerrado (estado 1) y los demás abiertos (estado 0):
Se podría pretender representar el estado 0000 0001, o simplemente el estado 1. La siguiente figura representa alguno de los diferentes estados que podría adaptar el dispositivo: 0
Estado 0000 0010 o estado 2
Estado 0000 0011 o estado 3
Estado 0101 1011 o estado 91
8
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Como se puede apreciar en un dispositivo como este se pueden obtener 256 estados diferentes, por lo tanto se puede relacionar cada uno de estos estados con valores que van de 0 a 255. Dispositivos análogos a éste, conforman cada una de las celdas de memoria del computador. En términos generales se denomina “Byte” a una unidad de información compuesta por ocho bits. Bueno, con este dispositivos podemos almacenar números que van de 0 a 255, ¿qué se hace para almacenar números de mayor tamaño? La respuesta es simple, agrupando más de una celda de memoria, y luego tomando la representación del conjunto, así: 65,536 estados diferentes
4,294’967,296 estados diferentes
Con esto, prácticamente se está solucionando el problema de los números de gran tamaño, sin embargo se debe tomar en cuenta que los sistemas definidos en el computador no dan la libertad de representar cualquier número, los sistemas definen primero bajo qué esquema se representarán sus números, empleando un byte, dos bytes, cuatro bytes, etc., una vez definido esto, el usuario sólo podrá emplear un rango de números de acuerdo a ese esquema. Así, si el sistema define una representación de 2 bytes para sus números, no se podrá manejar por ejemplo el número 70,000.
Representación de números negativos La pregunta que viene ahora es ¿qué pasa con los números negativos? Para solucionar este problema, se debe tener en cuenta, primero que el número se debe representar en un esquema de bytes, el signo “-” tiene que entrar en este esquema, y en segundo lugar, que operar dos números bajo esta representación debe ser una tarea extremadamente sencillas. Por último, si la operación da como respuesta un valor en el que se requieran más bits para representar el resultado, el valor resultante se almacenará sin el bit más significativo, éste se pierde o se acarrea según el sistema o el programa que se esté empleando.
9
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Suma de bits:
Suma de bytes:
0 1 0 1
+ + + +
0 0 1 1
= = = =
0 1 1 0
Se acarrea 1
0000 0000 + 0000 0001 = 0000 0001 0000 0001 + 0000 0001 = 0000 0010 1111 1111 + 0000 0001 = 0000 0000
Se acarrea 1
En este sentido, la representación de números negativos debe permitir realizar operaciones tan sencillas como la que se muestra en la tabla anterior. A continuación se muestran tres maneras de representarlos, analizando la posibilidad de hacer con ellas operaciones sencillas. Método 1: Empleo de un bit para representar el signo Este método consiste en emplear un bit, de los ocho que tiene un byte, para representar el signo. Esto es 0 para números positivos y 1 para negativos. Los restantes bits se empalan para representar el número. Lo primero que se debe apreciar es que en un byte ya no se podrán representar números que vayan de 0 a 255, sino que ahora sólo se podrá representar valores de 0 a 127, esto debido a que se cuenta con tan solo 7 bits, el octavo se emplea para el signo. Bajo este esquema, el número 27 se representaría como 0001 1011 y -27 se representa como 1001 1011. El esquema de representación es sencillo, sin embargo al querer hacer operaciones con estos números se presentan estas situaciones: 27 + 9 ---36 -27 + -9 ----36 27 + -9 ---18
0001 1011 + 0000 1001 ----------0010 0100 1001 1011 + 1000 1001 ---------0010 0100 El acarreo final se pierde . 0001 1011 + 1000 1001 ---------1010 0100
0010 0100 es la representación de 36, la operación es correcta. 0010 0100 representa 36 y no -36, la operación es incorrecta.
1010 0100 representa -36 y no 18.
Como se puede apreciar, no es tan sencillo sumar dos números en esta representación, se tendría que agregar condicionales para que la operación sea hecha en forma correcta, y esto no se puede aceptar. Método 2: Complemento Este método consiste en representar un número negativo como el complemento del número positivo, esto es 27 se representa como 0001 1011 y -27 como 1110 0100, observe que en el primer caso el bit más significativo es 0 y en el segundo caso es 1, aquí también se podrán representar números que van sólo de -127 a 127. Veamos cómo se comporta esta representación cuando se desea hacer una suma: 10
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
27 + 9 ---36
0001 1011 + 0000 1001 ---------0010 0100
0010 0100 es la representación de 36, la operación es correcta.
-27 + 9 ----18
1110 0100 + 0000 1001 ---------1110 1101
1110 1101 es el complemento de 0001 0010 y esto representa 18, por lo tanto la operación es correcta.
27 + -9 ---18
0001 1011 + 1111 0110 ---------0001 0001
0001 0001 representa 17 y no 18. La operación es incorrecta
-27 + -9 ----36
1110 0100 + 1111 0110 ---------1101 1010
1101 1010 es el complemento de 0010 0101 y esto representa 37 y no 36 por lo tanto la respuesta no es correcta.
Nos encontramos en un caso similar al anterior, la representación de los números es sencilla pero las operaciones con ellos no. Método 3: Complemento a 2 Este método consiste en representar un número negativo como el complemento del número positivo pero a esta representación se le suma 1, esto es 27 se representa como 0001 1011 y -27 como 1110 0101 (a diferencia de la representación anterior que daba 1110 0100), observe que, de la misma forma, en el primer caso el bit más significativo es 0 y en el segundo caso es 1 y por lo tanto se podrán representar números que van de -128 a 127. Veamos cómo se comporta esta representación cuando se desea hacer una suma: 27 + 9 ---36
0001 1011 + 0000 1001 ---------0010 0100
0010 0100 es la representación de 36, la operación es correcta.
-27 + 9 ----18
1110 0101 + 0000 1001 ---------1110 1110
1110 1110 es el complemento a 2 de 0001 0010 y esto representa 18, por lo tanto la operación es correcta.
27 + -9 ---18
0001 1011 + 1111 0111 ---------0001 0010
0001 0010 representa 18, la operación es correcta.
-27 + -9 ----36
1110 0101 + 1111 0111 ---------1101 1100
1101 1100 es el complemento a 2 de 0010 0100 y esto representa 36 por lo tanto la respuesta es correcta.
Esta es una representación sencilla en la que se pueden hacer operaciones sencillas sin que, como en los otros casos, tengan que introducir condicionales. La representación de números enteros en complemento a 2 es un estándar por lo que todos los lenguajes de programación lo emplean. 11
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Representación de números reales o de punto flotante Una característica de los número reales es que si tomamos dos números cualesquiera que pueden estar muy juntos, se puede probar que existen infinitos números entre esos dos números. Así por ejemplo, si tomamos el 2 y el 3, podemos encontrar el 2.1, 2.2, 2.3,… 2.9, pero también el 2.01, 2.02,… 2.09, 2.11, 2.12,… y así sucesivamente. Esta propiedad hace que para representar números reales se haya tenido que definir una forma mucho más compleja que para los números enteros. Más aun, los diferentes autores no han podido ponerse de acuerdo sobre el formato final que se les da a los números reales de modo que los diferentes lenguajes de programación emplean distintos formatos para el manejo de sus números reales, por ejemplo el Pascal emplea 6 bytes para representar un número real, mientras que C lo hace con 4, por esta razón la información que sale de un lenguajes no puede ser empleada con facilidad por otro lenguaje. Esto no se da con los enteros ya que allí si se emplea un estándar. De lo que sí se puede hablar de común que existen entre los formatos de números reales es en el principio que se emplea para representarlos en memoria. El método utilizado se base en la “notación científica normalizada”, esta notación se basa en que un número se multiplique o divida varias veces por la base en la que esté representado hasta que todos los dígitos menos uno aparezcan a la derecha del punto decimal, de modo que la parte entera del número sea mayor que cero. Por ejemplo: El número El número El número El número El número El número etc.
157.381 (10) puede ser representado como 1.57381x102 0.00007281 (10) puede ser representado como 7.281x10-5 -7800000000.0 (10) como -7.8x109 - 0.00000000000456 (10) como -4.56x10-15 10101.1101 (2) como 1.01011101x24 0.00F3A (16) como F.3Ax16-3
En términos generales se puede afirmar que un número cualquiera R expresado en una base b, se puede representar como:
R = m x be Donde “m” es un número cuya parte entera es de un solo dígito mayor que cero, “b” es la base del sistema y “e” es un número entero que denota cuántas veces se desplazó el punto decimal, a la derecha (+) o izquierda (-), para obtener el valor de “m” a partir de “R”. En otras palabras el número está compuesto por una “mantisa multiplicada por una “base” elevada a una potencia entera denominada “exponente”. La ventaja de este método es que se pueden representar números muy grandes y también extremada mente pequeños como 1.3456x10 23 ó 8.09234x10-56. Veamos ahora cómo esta notación científica puede ser empleada por los computadores para representar los números de punto flotante. Primero se decide cuantos bytes se van a utilizar en esta representación, por ejemplo Pascal emplea 6 bytes para sus datos de tipo real, por otro lado el lenguaje C emplea 4 bytes para representar el tipo float. 12
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
En el caso del lenguaje C, la representación de 4 bytes (32 bits), se distribuirían de la siguiente manera: un bit para el signo del número, 8 bits para representar el exponente, el resto (23) queda para la mantisa. En esta representación no se emplea el complemento a dos para la mantisa por lo que el bit de signo indicará positivo con un cero (0) y negativo con un uno (1). Por otro lado debido a que el sistema binario (en base 2) sólo se emplea los dígitos 0 y 1, la parte entera de la mantisa siempre será 1, este dígito no se lleva a la representación en el computador como se verá más adelante. Para manejar el exponente, tampoco se emplea la notación en complemento a dos debido a que complicaría las operaciones con los números de ponto flotante. Por esto debido, a que el exponente estaría en el rango de -126 á 127, éste se desplaza en 127, con lo que se obtienen valores entre 1 y 254. El exponente cero (0) se emplea para denotar el número 0 mientras que el valor 255 para denotar el valor infinito ( ). Ejemplos: 1) El número 734.8267 de representaría de la siguiente manera: Bit de signo: 0 (número positivo) Mantisa: 734.8267 (10) = 1011011110.110100111010001010011100011… (2) Mantisa normalizada: 1.011011110110100111010001001110001111001… x 2 9 Exponente: 9, normalizado = 136(10) (9+127) 10001000(2) Por lo tanto el número quedará representado como: Signo Exponente 0 10001000
Mantisa 01101111 01101001 1101001
Separado en bytes tendíamos: o, en hexadecimal:
01000100
00110111
10110100
11101001
44
37
B4
E9
2) El número -0.000008267 de representaría de la siguiente manera: Bit de signo: 1 (número negativo) Mantisa: 0.000008267 (10) =
0.00000000000000001000101010110010011111110100000011101
Mantisa normalizada: 1.000101010110010011111110100000011101…x 2 -17 Exponente: -17, normalizado = 110(10) (-17+127) 01101110 (2) Por lo tanto el número quedará representado como: Signo Exponente 1 01101110
Mantisa 00010101 01100100 1111111
Separado en bytes tendíamos: 10110111
00001010
10110010
01111111
B7
0A
B2
7F
o, en hexadecimal: 13
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
En el caso del lenguaje Pascal, los números de punto flotante se representan en 6 bytes (48 bits), los cuales se distribuyen de la siguiente manera: primero va el exponente de 8 bits para, le sigue un bit para el signo del número y el resto (39 bits) para la mantisa. Nótese que la primera diferencia con respecto al C, sin contar el número de bits empleados, es la posición del signo, en C va primero mientras que en Pascal va después del exponente. En esta representación tampoco se emplea el complemento a dos para la mantisa por lo que el bit de signo indicará positivo con un cero (0) y negativo con un uno (1). Para la mantisa se emplea otro estándar, en éste, la notación se basa en que un número se multiplique o divida varias veces por la base en la que esté representado hasta que todos los dígitos aparezcan a la derecha del punto decimal, esto es, el número 157.381(10) se representa como 0.157381x10 3 a diferencia del otro método en el que se obtiene 1.57381x102. La mantisa se toma a partir del punto decimal. Finalmente, en la representación del número se toma a partir del segundo dígito, esto porque el primer dígito siempre será 1. Para manejar el exponente, a éste se le suma 128, en lugar de 127 como se hace en C. Ejemplos: 1) El número 734.8267 de representaría de la siguiente manera: Mantisa: 734.8267 (10) = 1011011110.110100111010001010011100011110… (2) Mantisa normalizada: 0.1011011110110100111010001010011100011110… x 2 10 Exponente: 10, normalizado = 138(10) (10+128) 10001010(2) Bit de signo: 0 (número positivo) Por lo tanto el número quedará representado como: Exponente 10001010
Signo 0
Mantisa 01101111 01101001 11010001 01001110 0011110
Separado en bytes tendíamos: 10001010
00110111
10110100 11101000 10100111 00011110
o, en hexadecimal: 8A
37
B4
E8
A7
1E
2) El número -0.000008267 de representaría de la siguiente manera: Mantisa: 0.000008267 (10) = 0.00000000000000001000101010110010011111101010000001111000 Mantisa normalizada: 0.1000101010110010011111101010000001111000…x 2-16 Exponente: -16, normalizado = 112(10) (-16+120) 01110000 (2) Bit de signo: 1 (número negativo) Por lo tanto el número quedará representado como: Exponente 01110000
Signo 1
Mantisa 00010101 01100100 11111101 01000000 1111000
14
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Separado en bytes tendíamos: 01110000
10001010
10110010 01111110 10100000 01111000
o, en hexadecimal: 70
8A
B2
7E
A0
78
Almacenamiento invertido o “back-words” Como hemos podido apreciar en detalle hasta este momento, como se representan los valores numéricos en el computador, hemos apreciado que se requiere muchas veces más de un byte para poder representarlos. Sin embargo nos falta un concepto que es muy importante cuando se estudia la representación interna de datos, este concepto se denomina “almacenamiento invertido de palabras o back-words. Cuando nosotros visualizamos un número éste lo leemos de izquierda a derecha, desde la cifra más significativa, esto es por ejemplo el número novecientos cuarenta y siete (947) la cifra de mayor valor (la más significativa), el 9, se encuentra a la izquierda del número, mientras que la de menos valor, el 7, se encuentra a la derecha. Esto no tiene nada de particular puesto que así estamos acostumbrados a representarlos, sin embargo, podemos darnos cuenta que ese no es el orden en que operaríamos dos números, las operaciones se hacen de derecha a izquierda, empezando de la cifra menos significativa, esto es 5638 + 294 2 Para en computador, leer un número en un sentido y luego operarlo en el otro resulta una tarea muy complicada, por esta razón en el computador los números se almacenan empezando de la parte menos significativa hasta la más significativa. Esto quiere decir que, por ejemplo, si el computador trabajara en base 10 el número novecientos cuarenta y siete se almacenaría como 749 (y se leería siete cuarenta novecientos) a esta forma de almacenamiento o manejo de los números se denomina back-words. De ese modo se operaría el número en el mismo sentido al que se lee. Por ejemplo: El número entero 8049653 representado en binario empleando 4 bytes como 00000000 01111010 11010011 11110101 (00 7A D3 F5(16)) se almacena en la memoria del computador como 11110101 11010011 01111010 00000000 (F5 D3 7A 00(16)), fíjese que la representación inversa es a nivel de bytes y no de bits. En el caso de los números reales, en los ejemplos se tenía: Para el número 734.8267 se obtenía para el lenguaje C, 44 37 B4 E9, en memoria se almacenaría como E9 B4 37 44. El mismo número en Pascal se obtenía 8A 37 B4 E8 A7 1E su representación en memoria será 8A 1E A7 E8 B4 37, obsérvese que en el caso del lenguaje C todo el número se escribe al revés, sin embargo en el Pascal, sólo se invierte la mantisa del número. De igual manera, el número -0.000008267 que en C se representaría como B7 0A B2 7F en memoria se almacenaría como 7F B2 0A B7, y en Pascal su representación sería 70 8A B2 7E A0 78 y en memoria se almacena 70 78 A0 7E B2 8A. 15
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Representación de caracteres El computador sólo puede representar códigos binarios, lo hemos visto con los números. El caso de los caracteres no es una excepción, para almacenar en la memoria del computador un caracter, como la letras A, el símbolo #, etc., se requiere codificar el caracter en un formato binario. A lo largo del tiempo se han venido utilizando diferentes sistemas de codificación, se empezó con el sistema BCDIC o Binary Coded Decimal Interchange Code (código binario de intercambio decimal), en él se agruparon los 64 caracteres más comunes y a cada uno se le relacionó con un número entero de 0 a 63, el valor binario del número corresponde al código del caracter. Como se puede deducir este sistema se representaba en 6 bits. Por los años setenta se termina de implementar otro sistema de codificación denominado ASCII o American Standard Code for Information Interchange este sistema empezó empleando 7 bits para representar 128 caracteres que incluían las letras mayúsculas y minúsculas del alfabeto inglés así como caracteres de puntuación y de control. Sin embargo, con el paso del tiempo se amplió el código ASCII a 8 bits para poder incluir caracteres internacionales como la Ñ, á, ü, æ, ç etc. El código ASCII extendido, o por costumbre simplemente ASCII, se popularizó muchísimo y hasta hoy se viene empleando en forma masiva en todo el mundo, sin embargo se debe tener en cuenta que si bien los 128 primeros caracteres son siempre los mismos, el segundo grupo puede variar de acuerdo a la localidad donde se emplee. El juego de caracteres ASCII más común, se muestra en la tabla que a continuación se presenta. Nótese que cada caracter viene acompañado de un código numérico (dado como un número decimal y como un número hexadecimal). Es este código y no el caracter es el que se almacena en la memoria del computador como cualquier valor entero, así por ejemplo un la letra A se almacena en la memoria como el número 65 codificado en binario. Usted se preguntará entonces cómo se hace para diferenciar un caracter de un número entero almacenado en la memoria, la respuesta en es que no hay forma, serán las funciones de los lenguajes de programación las que harán la diferencia. Por ejemplo en el lenguaje C, a una variable entera asignada con el valor de 65 puede mostrarse en la pantalla como el número 65, como el número 41h o como el caracter A y esto sólo depende de la manera cómo se llame a la función de salida. Esto es: x=65; printf(“%d”, x); muestra el número decimal 65 printf(“%x”, x); muestra el número hexadecimal 41 printf(“%c”, x); muestra el caracter A
Observe también que en la tabla los caracteres vienen agrupados, esto es las letras mayúsculas vienen juntas desde la posición 65 a la 90, igual pasa con las letras minúsculas y los dígitos. Note también que entre el caracter “A” y el caracter “a” existen 32 posiciones. Esto se ha hecho para podes manipular los caracteres en forma sencilla, si se tiene almacenado el código de la letra H (72) fácilmente se puede obtener el caracter I, para esto sólo hay que sumarle uno al código de la letra H, por otro lado si le sumamos 32 al código de la letra H obtenemos el caracter h. 16
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo C Ó D I G O
C Ó D I G O
D E C
H E X
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F
C A R A C T E R
☺ ☻ ♥ ♦ ♣ ♠ •
◘ ○ ◙ ♂ ♀ ♪ ♫ ☼ ► ◄ ↕ ‼
¶ § ▬ ↨ ↑ ↓ → ← ∟ ↔ ▲ ▼
C Ó D I G O
C Ó D I G O
D E C
H E X
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
20 21 22 23 24 25 26 27 28 29 2A 2B 2C 2D 2E 2F 30 31 32 33 34 35 36 37 38 39 3A 3B 3C 3D 3E 3F
C A R A C T E R
! " # $ % & ' ( ) * + . / 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
C Ó D I G O
C Ó D I G O
D E C
H E X
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
40 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F
C A R A C T E R
C Ó D I G O
C Ó D I G O
D E C
H E X
@ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z [ \ ] ^ _
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 12 0 121 122 123 124 125 126 127
60 61 62 63 64 65 66 67 68 69 6A 6B 6C 6D 6E 6F 70 71 72 73 74 75 76 77 78 79 7A 7B 7C 7D 7E 7F
C A R A C T E R
C Ó D I G O
C Ó D I G O
D E C
H E X
` a b c d e f g h i j k l m n o p q r s t u v w x y z { | } ~
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159
80 81 82 83 84 85 86 87 88 89 8A 8B 8C 8D 8E 8F 90 91 92 93 94 95 96 97 98 99 9A 9B 9C 9D 9E 9F
⌂
C A R A C T E R
C Ó D I G O
C Ó D I G O
D E C
H E X
Ç ü é â ä à å ç ê ë è ï î ì Ä
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 æ 177 Æ 178 ô 179 ö 180 ò 181 û 182 ù 183 ÿ 184 Ö 185 Ü 186 ¢ 187 £ 188 ¥ 189 190 ₧ ƒ 191
TABLA DE CARACTERES ASCII
A0 A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 B1 B2 B3 B4 B5 B6 B7 B8 B9 BA BB BC BD BE BF
C A R A C T E R
C Ó D I G O
C Ó D I G O
D E C
H E X
á í ó ú ñ Ñ ª º ¿ - ¬ ½ ¼ ¡ « »
192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223
C0 C1 C2 C3 C4 C5 C6 C7 C8 C9 CA CB CC CD CE CF D0 D1 D2 D3 D4 D5 D6 D7 D8 D9 DA DB DC DD DE DF
▒ ▓ │ ┤ ╡ ╖ ╕ ╣ ║ ╗ ╝ ╜ ╛ ┐
C A R A C T E R
C Ó D I G O
C Ó D I G O
D E C
H E X
└ ┴ ┬ ├ ─ ┼ ╞ ╟ ╚ ╔ ╩ ╦ ╠ ═
224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
E0 E1 E2 E3 E4 E5 E6 E7 E8 E9 EA EB EC ED EE EF F0 F1 F2 F3 F4 F5 F6 F7 F8 F9 FA FB FC FD FE FF
╧ ╤ ╥ ╙ ╘ ╒ ╓ ╫ ╪ ┘ ┌ █ ▄ ▌ ▐ ▀
C A R A C T E R
α β Γ Π Σ σ μ τ Φ Θ Ω Δ ∞ Φ Є Ω Ξ
± ≥ ≤ ∫ ∫
÷ ≈
° ●
· √ ⁿ
² ■
17
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Como se dijo anteriormente, el código ASCII tiene un problema, este es que los caracteres de la segunda mitad de la tabla no son siempre los mismos lo que hace muy difícil su transporte a otras localidades. Por los años 90 se propuso un nuevo sistema de codificación denominado Unicode en los que cada caracter es codificado en dos bytes (16 bits). Este sistema permite la representación de de 65535 caracteres con lo que con esto queda prácticamente cubierto todos los símbolos del planeta. Unicode se ha convertido en un sistema de codificación cuyo objetivo es proporcionar el medio por el cual un texto en cualquier forma e idioma pueda ser codificado para el uso informático. Unicode cubre la mayor parte de las escrituras usadas actualmente, incluyendo: Árabe, Armenio, Bengalí , Braille, Sílabas aborígenes canadienses, Cheroqui, Copto, Cirílico, Devanāgarī , Esperanto, Ge'ez, Georgiano, Griego, Guyaratí, Gurmukhi, Hangul (Coreano), Han (Kanji, Hanja y Hanzi), Japonés (Kanji, Hiragana y Katakana), Hebreo, Jémer (Camboyano), Kannada (Canarés), Lao, Latino, Malayalam, Mongol, Burmese, Oriya, Syriac, Tailandés (Thai), Tamil, Tibetano, Yi, Zhuyin (Bopomofo). Unicode ha ido añadiendo escrituras y cubrirá aún más, incluyendo escrituras históricas menos utilizadas, incluso aquellas extinguidas para propósitos académicos: Cuneiforme, Griego antiguo, Linear B, Fenicio, Rúnico, Sumerio, Ugarítico. Los jeroglíficos egipcios han sido propuestos para la versión de UNICODE 4.1 1
Representación de cadenas de caracteres La representación de cadenas de caracteres no está estandarizada, esto quiere decir que los diferentes lenguajes de programación no manejan las cadenas de caracteres de la misma manera, a pesar que el manejo de cadenas se basa en la aglomeración de caracteres, los cuales como hemos visto si están estandarizados. En Pascal por ejemplo las cadenas de caracteres se basan en un tipo de datos denominado string. Este tipo de datos define las cadenas como un arreglo de 256 caracteres, cuyos índices van de 0 a 255. A partir de la posición 1 se colocan los caracteres que se quiere almacenar en la cadena, en la posición 0 se almacena un caracter cuya representación numérica corresponde a la cantidad de caracteres que se almacenó en la cadena. Por ejemplo: Si se quiere almacenar la siguiente cadena de caracteres: ‘Rodolfo Alexander López Rojas’ Esto se haría de la siguiente manera: ↔ 0
R 1
o 2
d 3
o 4
l 5
f 6
o 7
8
A 9
l 0 1
e 1
x 2
a 3
n 4
d 5
e 6
r 7
8
L 9
ó p 0 1 2
e 2
z 3
4
R 5
o j a 6 7 8
s 9
… 0 3
Nótese que en la posición 0 del arreglo se coloca el caracter ‘ ↔’ que corresponde al código 29 en la tabla ASCII. Cualquier operación que se realice sobre la cadena afectará en forma automática la posición cero del arreglo de modo que en cada momento se tenga la cantidad de caracteres válidos almacenados. 1
Tomado de : http://es.wikipedia.org/wiki/Unicode
18
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Es bueno notar que debido a que la longitud de la cadena se denota por un solo caracter es imposible que esta representación pueda almacenar más de 255 caracteres. Otros lenguajes de programación manejan sus cadenas de caracteres de manera muy diferente. Por ejemplo el lenguaje C, la representación de cadenas se hace por medio de arreglos, pero también por medio de direcciones de memoria (punteros) a espacios de memoria gestados dinámicamente (en tiempo de ejecución de un programa). Aquí los caracteres son colocados a la manera de un arreglo pero no se coloca la longitud en el inicio, más bien se coloca un caracter especial de terminación al final de los caracteres. En este caso se coloca el caracter cuyo código ASCII es cero. Todo trabajo efectuado sobre la cadena se empieza desde el inicio de la misma, hasta encontrar el caracter de terminación. Por ejemplo la cadena: ‘Rodolfo Alexander López Rojas’
Se almacenaría de la siguiente manera: R 0
o 1
d 2
o 3
l 4
f 5
o 6
7
A 8
l 9
e 0 1
x 1
a 2
n 3
d 4
e 5
r 6
7
L ó 8 9
p 0 2
e 1
z 2
3
R 4
o j 5 6
a s 7 8
‘\0’ 9
… 0 3
‘\0’ representa al caracter cuyo código ASCII es cero
Esto permite que las cadenas no tengan límite de tamaño, lo que puede ser una gran ventaja en algunos casos.
Otras representaciones más complejas Veamos ahora cómo se representan elementos más complejos en el computador, por ejemplo una imagen. Una figura, una fotografía, un dibujo, etc., para que pueda almacenarse en el computador tendrán que de alguna manera llevarse a una representación basada en ceros y unos. Si cogemos una fotografía y la observamos a través de un lente de aumento podemos observar que ésta no es impresa de manera continua como cuando se coge un pincel y se hace un trazo con él. Lo que observamos es que la fotografía está compuesta por una serie de puntos muy pequeños colocados lo suficientemente juntos como para que, vista la imagen a una distancia prudencial, parezca que esté formada por trazos continuos. Si observamos un monitor de computador o televisión podemos observar el mismo esquema. Las imágenes se descomponen en pequeños elementos a los que se le asignan un color determinado, el conjunto visto desde una distancia prudencial forma una imagen. Precisamente del inglés “picture element” deriva el nombre píxel, conocido como el elemento más pequeño en que se descompone una imagen. La pregunta ahora es saber cómo representamos un píxel en términos de ceros y unos. Empecemos imaginándonos una figura en blanco y negro, para representar cada píxel sólo necesitaremos dos valores, uno para representar el blanco y otro para el negro, podemos asignar al blanco el valor de cero y uno al negro. De esta forma sólo necesitamos un bit para representar un píxel y un byte puede almacenar la representación de 8 píxeles. A una imagen que tuviera por ejemplo 4 colores, por ejemplo azul, verde, rojo y amarillo, podríamos asignarle cero al azul, uno al verde, dos al rojo y tres al amarillo. Aquí podemos observar que requerimos dos cifras binarias para representar cada color 19
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
(azul = 00, verde = 01, rojo = 10, amarillo = 11), por lo tanto dos bits por cada píxel. Si quisiéramos almacenar una imagen que tenga millones de colores quizá requiramos cuatro o más “bytes” para representar cada píxel. En el ejemplo de la página siguiente vemos dos figuras y a su lado la misma imagen pero descompuesta en los valores que tendría cada píxel. En el primer caso la figura es en blanco y negro, en la otra empleamos hasta 8 colores diferentes. Mientras que en la primera se requiere sólo un digito binario para representar cada píxel en la segunda se requieren tres dígitos binarios por cada píxel. Ya tenemos definido la manera que se codifica cada píxel, sin embargo aun no podemos decir que el problema está solucionado, no es suficiente con esta información. Un programa que pueda manipular imágenes debe ser capaz de procesar diferentes tipos de imágenes, de lo contrario se tendría que tener un programa para leer imágenes en blanco y negro, otro para imágenes con cuatro colores, otro para imágenes de 8 colores y así sucesivamente. Por otro lado se debería tener un programa por cada tamaño de imagen que se quisiera procesar (combinado con la cantidad de colores). Se podría pensar en miles de programas. Esto es ilógico, un solo programa debe ser capaz de procesar cualquier imagen. Es por esto que el formato que se emplee para almacenar una imagen en el computador de una imagen debe incluir de alguna manera la cantidad de colores o el número de bits que se requieren para representar un píxel y el número de píxeles que tiene cada línea y el número de líneas que tiene la imagen. En este sentido se podría almacenar en el primer byte la cantidad de bits que se requieren para representar cada píxel, en los dos bytes siguientes se puede almacenar un número entero sin signo con la cantidad de píxeles por fila que tiene la imagen y en los dos bytes siguiente, con el mismo formato, la cantidad de filas de la imagen. Con esa información un programa podría reconstruir la imagen.
20
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo Valor de cada píxel: 1 píxel = 1 bit 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 1,0,1,0,0,0,0,1,1,1,0,0,0,0,1,0,0,1 1,0,0,1,0,0,1,1,1,1,1,0,0,1,0,0,0,1 1,0,0,0,1,0,1,1,1,1,1,0,1,0,0,0,0,1 1,0,0,0,0,1,0,1,1,1,0,1,0,0,0,0,0,1 1,0,0,0,0,0,1,0,1,0,1,0,0,0,0,0,0,1 1,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,1 1,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,1 1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1 1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1 1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,1 1,0,0,0,1,1,1,0,0,0,1,1,1,0,0,0,0,1 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 Valor decimal de cada píxel. 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 1,3,2,3,3,3,3,4,4,4,3,3,3,3,2,3,3,1 1,3,3,2,3,3,4,4,4,4,4,3,3,2,3,3,3,1 1,3,3,3,2,3,4,4,4,4,4,3,2,3,3,3,3,1 1,3,3,3,3,2,3,4,4,4,3,2,3,3,3,3,3,1 1,3,3,3,3,3,2,3,4,3,2,3,3,3,3,3,3,1 1,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,3,6,3,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,3,6,3,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,3,6,3,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,6,3,6,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,6,3,6,3,3,3,3,3,3,3,1 1,3,3,3,3,3,3,6,3,6,3,3,3,3,3,3,3,1 1,3,3,3,3,3,6,3,3,3,6,3,3,3,3,3,3,1 1,3,3,3,3,3,6,3,3,3,6,3,3,3,3,3,3,1 1,3,3,3,3,3,6,3,3,3,6,3,3,3,3,3,3,1 1,3,3,3,7,7,7,3,3,3,7,7,7,3,3,3,3,1 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
Valor de cada píxel: 1 píxel = 3 bits 001,001,001,001,001,001,001,001,001,001,001,001,001,001,001,001,001,001 001,011,010,011,011,011,011,100,100,100,011,011,011,011,010,011,011,001 001,011,011,010,011,011,100,100,100,100,100,011,011,010,011,011,011,001 001,011,011,011,010,011,100,100,100,100,100,011,010,011,011,011,011,001 001,011,011,011,011,010,011,100,100,100,011,010,011,011,011,011,011,001 001,011,011,011,011,011,010,011,100,011,010,011,011,011,011,011,011,001 001,011,011,011,011,011,011,010,010,010,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,011,010,011,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,011,010,011,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,011,010,011,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,011,110,011,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,011,110,011,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,011,110,011,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,110,011,110,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,110,011,110,011,011,011,011,011,011,011,001 001,011,011,011,011,011,011,110,011,110,011,011,011,011,011,011,011,001 001,011,011,011,011,011,110,011,011,011,110,011,011,011,011,011,011,001 001,011,011,011,011,011,110,011,011,011,110,011,011,011,011,011,011,001 001,011,011,011,011,011,110,011,011,011,110,011,011,011,011,011,011,001 001,011,011,011,111,111,111,011,011,011,111,111,111,011,011,011,011,001 001,001,001,001,001,001,001,001,001,001,001,001,001,001,001,001,001,001
21
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
A continuación se muestra cómo quedaría la representación de cada figura. Figura 1: Byte 1 0000 0001 1 bit x píxel
Bytes 2 y 3 0000 0000 0001 0010 18 píxeles x fila
Bytes 4 y 5 0000 0000 0001 0101 21 filas
Byte 6 1111 1111
Byte 7 1111 1111
Byte 11 0011 1110
Byte 12 0100 0110
Byte 13 0010 1111
Byte 14 1010 0001
Byte 21 0000 0110
Byte 22 0000 0010
Byte 23 0000 0001
Byte 31 0000 0010
Byte 32 0000 0001
Byte 41 0000 0001 Byte 51 1111 1111
Byte 8 Byte 9 1110 1000 0111 0000 Valor de cada pixel
Byte 10 1001 1001
Byte 15 1000 0101
Byte 16 1101 0000
Byte 17 0110 0000
Byte 18 1010 1000
Byte 19 0001 1000
Byte 20 0001 1100
Byte 24 1000 0000
Byte 25 1000 0000
Byte 26 0110 0000
Byte 27 0010 0000
Byte 28 0001 1000
Byte 29 0000 1000
Byte 30 0000 0110
Byte 33 1000 0000
Byte 34 1000 0000
Byte 35 0110 0000
Byte 36 0101 0000
Byte 37 0001 1000
Byte 38 0001 0100
Byte 39 0000 0110
Byte 40 0000 0101
Byte 42 1000 0010
Byte 43 0010 0000
Byte 44 0110 0000
Byte 45 1000 1000
Byte 46 0001 1000
Byte 47 0010 0010
Byte 48 0000 0110
Byte 49 0011 1000
Byte 50 1110 0001
Byte 52 1111 1111
Byte 53 1100 0000
Byte 7 Byte 8 Byte 9 1001 0010 0100 1001 0010 0100 Valor de cada pixel
Byte 10 1001 0010
Figura 2 Byte 1 0000 0011 3 bits x píxel
Bytes 2 y 3 0000 0000 0001 0010 18 píxeles x fila
Bytes 4 y 5 0000 0000 0001 0101 21 filas
Byte 6 0010 0100
Byte 11 0100 1001
Byte 12 0010 0100
Byte 13 1011 0100
Byte 14 1101 1011
Byte 15 0111 0010
Byte 16 0100 0110
Byte 17 1101 1011
Byte 18 0100 1101
Byte 19 1001 0010
Byte 20 1101 1010
Byte 21 0110 1110
Byte 22 0100 1001
Byte 23 0010 0011
Byte 24 0110 1001
Byte 25 1011 0110
Byte 26 0100 1011
Byte 27 0110 1101
Byte 28 0011 1001
Byte 29 0010 0100
Byte 30 1000 1101
Byte 31 0011 0110
Byte 32 1101 1001
Byte 33 0010 1101
Byte 34 1011 0110
Byte 35 1001 1100
Byte 36 1001 0001
Byte 37 1010 0110
Byte 38 1101 1011
Byte 39 0110 0100
Byte 40 1011 0110
Byte 41 1101 1011
Byte 42 0100 1110
Byte 43 0011 0100
Byte 44 1101 1011
Byte 45 0110 1101
Byte 46 1001 0010
Byte 47 1101 1011
Byte 48 0110 1101
Byte 49 1010 0100
Byte 50 1001 1011
Byte51 0110 1101
Byte 52 1011 0110
Byte 53 0100 1011
Byte 54 0110 1101
Byte 55 1011 0110
Byte 56 1101 0011
Byte 57 0110 1101
Byte 58 1011 0110
Byte 59 1101 1001
Byte 60 0010 1101
Byte 61 1011 0110
Byte 62 1101 1011
Byte 63 0100 1101
Byte 64 1011 0110
Byte 65 1101 1011
Byte 66 0110 0100
Byte 67 1011 0110
Byte 68 1101 1011
Byte 69 0110 1101
Byte 70 0011 0110
Byte 71 1101 1011
Byte 72 0110 1101
Byte 73 1001 0010
Byte 74 1101 1011
Byte 75 0110 1101
Byte 76 1011 1100
Byte 77 1101 1011
Byte 78 0110 1101
Byte 79 1011 0110
Byte 80 0100 101
Byte 81 1011 0110
Byte 82 1101 1011
Byte 83 0111 1001
Byte 84 1011 0110
Byte 85 1101 1011
Byte 86 0110 1100
Byte 87 1001 0110
Byte 88 1101 1011
Byte 89 0110 1101
Byte 90 1110 0110
Byte 91 1101 1011
Byte 92 0110 1101
Byte 93 1011 0010
Byte 94 0101 1011
Byte 95 0110 1101
Byte 96 1011 1100
Byte 97 1111 0011
Byte968 0110 1101
Byte 99 1011 0110
Byte 90 1100 1001
Byte 101 0110 1101
Byte 102 1011 0110
Byte 103 1111 0011
Byte 104 1100 1101
Byte 105 1011 0110
Byte 106 1101 1011
Byte 107 0010 0101
Byte 108 1011 0110
Byte 109 1101 1011
Byte 110 1100 1111
Byte 111 0011 0110
Byte 112 1101 1011
Byte 113 0110 1100
Byte 114 1001 0110
Byte 115 1101 1011
Byte 116 0111 1001
Byte 117 1011 0111
Byte 118 1001 1011
Byte 119 0110 1101
Byte 120 1011 0010
Byte 121 0101 1011
Byte 122 0110 1101
Byte 123 1110 0110
Byte 124 1101 1110
Byte 125 0110 1101
Byte 126 1011 0110
Byte 127 1100 1001
Byte 128 0110 1101
Byte 129 1011 0111
Byte 130 1001 1011
Byte 131 0111 1001
Byte 132 1011 0110
Byte 133 1101 1011
Byte 134 0010 0101
Byte 135 1011 0111
Byte 136 1111 1111
Byte 137 0110 1101
Byte 138 1111 1111
Byte 139 1101 1011
Byte 140 0110 1100
Byte 141 1001 0010
Byte 142 0100 1001
Byte 143 0010 0100
Byte 144 1001 0010
Byte 145 0100 1001
Byte 146 0010 0100
Byte 147 1001 0010
22
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Observe que la segunda imagen sólo tiene 8 colores, y es una figura pequeña; un imagen que ocupe toda la pantalla del computador en una resolución baja se maneja con 800x600 píxeles empleando millones de colores, imagínese el tamaño de esos archivos si se almacenara en un formato como el que describimos anteriormente. El formato empleado es muy simple pero no es óptimo, en el mercado existen muchos formatos para almacenar imágenes, algunos se basan en algoritmos que permiten compactar las imágenes de modo que ocupen menos espacio en memoria. Entre los formatos conocidos podemos mencionar BMP, JPEG, GIF, PDF, PCX, TIFF etc. Por último, imagínese usted cómo se puede almacenar en un computador un video, o sonidos, pues al igual que en los otros casos analizados, se tiene que dividir cada medio en elementos simples y estos llevarlos a representaciones binarias, no es una tarea fácil, pero si factible.
23
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 2: Software y lenguajes de programación
Conceptos generales de software Un computador está compuesto por dos elementos principales: el “ Hardware” y el “Software”. Mientras el hardware está referido a todos los componentes físicos del computador, es decir todo lo tangible que se encuentre en él, como el monitor, teclado, disco duro, etc., el software es el conjunto de programas que permiten realizar alguna tarea en el computador de modo que se pueda controlar éste con un fin específico. Al software también se le conoce como el componente lógico o intangible del computador. Se debe entender por “programa” al conjunto de órdenes que se proporcionan al computador y que éste luego ejecutará al pie de la letra, obteniendo un resultado esperado.
Clasificación del software El software se puede clasificar en tres grandes grupos: el “Software de sistema”, el “Software de aplicación” y el “Software de programación”. El software de sistema lo conforman programas que permiten controlar los diferentes dispositivos del computador como por ejemplo la impresora, el monitor, el disco duro, etc. Entre estos se pueden distinguir los sistemas operativos como el Windows, Linux, etc. también dentro de esta categoría de software está el BIOS (Basic Input/Output System). El software de aplicaciones permite solucionar problemas en general que no necesariamente tienen que ver con el computador. Un ejemplo de este tipo de software lo constituyen los procesadores de textos, los programas de diseño CAD (Computer Aided Design), los procesadores de imágenes, las hojas de cálculo, etc. El software de programación se refiere a los entornos de desarrollo IDE (Integrated Development Environment). Este tipo de software está conformado por editores de textos, compiladores y depuradores que permiten generar software de las tres categorías antes descritas. Un entorno de desarrollo en C permitió la creación de sistemas operativos como el Unix.
Sistema Operativo Un sistema operativo es un software que permita administrar los recursos del computador. Recursos como por ejemplo el procesador, los medios de almacenamiento (memoria RAM, disco duro, discos compactos y flexibles, etc.), los dispositivos de entrada y salida (teclado, monitor, impresora, etc.) los dispositivos de comunicación. Una tarea adicional de los sistemas operativos es la de proporcionar una interfaz entre el usuario y el computador de modo que se pueda establecer una comunicación entre el hombre y la máquina. Esta interfaz ha venido evolucionando a través del tiempo desde lo que se conoció como una interfaz de “ línea de comandos ” en donde el usuario del computador debía escribir un texto con la orden dada, hasta una interfaz gráfica basada en cajas de diálogo, ventanas, etc. 24
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
La figura que se muestra a continuación muestra la forma como se obtiene la lista de archivo almacenados en el disco duro en una carpeta determinada. Línea de comandos: 1) Aparece una línea de texto al final fi nal de la cual se escribe la orden:
2) Luego de escribir la orden se presiona la tecla “Enter ” ( )
3) El resultado aparece en modo texto:
25
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Interfaz Grafica 1) las órdenes se dan a través del “ Mouse”
Presionando el botón derecho
Presionando el botón izquierdo 2) Eligiendo las opciones a través de cajas ca jas de diálogo
Algoritmos La palabra algoritmo proviene del nombre del matemático persa llamado Mohamed AlKhowarizmi (léase Khowarizmi (léase “al-jouresmi”). Este personaje vivió en el siglo IX D.C. y alcanzó una gran reputación por sus enunciados en los que daba las reglas “paso a paso” para poder sumar, restar, dividir y multiplicar números decimales. La traducción al latín del apellido derivó en “algorismi” y posteriormente “algoritmo”. En este sentido podemos definir un algoritmo como “la descripción paso a paso de un método para resolver un problema” problema” . 26
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
En realidad, siempre trabajamos con algoritmos, piense en un problema cualquiera, “se nos pinchó un neumático y tenemos que reemplazarlo”, “necesitamos prepara un plato de comida”, “tenemos que calcular las raíces de una ecuación de segundo grado”, si usted es capaz de describir paso a paso cómo solucionar el problema entonces habrá creado un algoritmo. En las ciencias de la computación la definición de algoritmos es algo muy común e importante en la elaboración de programas, sin un algoritmo, no se puede escribir un programa. Por otro lado cuando hablamos de un algoritmo en programación estamos pensando en algo un poco más riguroso que un simple listado de tareas que hay que realizar, esto debido a que un algoritmo será ejecutado por un computador a través de un programa y a un computador no se le pueden dar órdenes como una persona se las da a otra. Todo algoritmo debe cumplir ciertas reglas que garanticen una correcta solución a un problema, es por esto que los algoritmos deben tener las siguientes características: Un algoritmo debe ser PRECISO PRECISO,, debe indicar claramente el orden de realización de cada paso. Un algoritmo no debe mostrar AMBIGÜEDADES AMBIGÜEDADES.. Un algoritmo debe ser DEFINIDO DEFINIDO,, es decir, si se sigue el algoritmo dos veces, se debe obtener el mismo resultado. Un algoritmo debe ser FINITO FINITO.. Si se sigue el algoritmo, se debe terminar en algún momento. Como se indicó anteriormente, un algoritmo debe reflejar la solución de un problema, y esta debe ser la mejor que podamos hallar. En este sentido debe entenderse que un algoritmo no es un programa de computador, es un paso previo para llegar a ese fin, por lo tanto un algoritmo no debe escribirse empleando terminologías propias de un lenguaje de programación, ya que los lenguajes de programación, como veremos más adelante, traen muchas limitantes. Escribir un algoritmo basándose en un lenguaje de programación haría que la solución se dé en función de las limitantes del lenguaje y no en la mejor solución. Una vez que se establezca el algoritmo de solución a un problema, se debe buscar un lenguaje de programación que pueda adaptarse a él, y no lo contrario. Por esta razón existen diferentes formas en las que podemos expresar un algoritmo, de modo que cualquier programador pueda entenderlo y pueda expresarlo en el lenguaje de programación que desee o que sea más adecuado. Entre las diferentes formas que podemos expresar un algoritmo, podemos mencionar las más comunes: Lenguaje Natural.Se refiere al mismo lenguaje empleado por las personas para comunicarse. Si bien es cierto que utilizar este lenguaje para describir un algoritmo es muy práctico, se debe tener mucho cuidado al hacerlo ya que se pueden introducir ambigüedades que harían deficiente el algoritmo. Frases como “Te vi con el telescopio” pueden telescopio” pueden parecer, en un principio, correctas pero si se pone a pensar, ¿quién tiene el telescopio? verá telescopio? verá que no es posible asegurar quién lo tiene. 27
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Expresar un algoritmo empleando un lenguaje natural podría ser más o menos como sigue: Para calcular el área de un triángulo, debemos: - Determinar cuál es valor de la base y altura del triángulo que queremos calcular. - Comprobamos que estos valores son positivos, de ser así calculamos el área empleando la siguiente fórmula
área =
base × altura 2
, de comprobarse que uno de los dos no es positivo no se
deberá calcular el área.
Pseudocódigo.Es un intermedio entre el lenguaje natural y un lenguaje de programación. Se emplean una serie de palabras claves como “leer “ leer”, ”, “imprimir “imprimir”, ”, “si… “si… entonces… de lo contrario…”, contrario…”, “inicio inicio”, ”, “fin fin”, ”, etc., esto con la finalidad de eliminar las ambigüedades que puede presentar el lenguaje natural y de no extender innecesariamente el algoritmo, pero sin llegar a la rigidez de un lenguaje de programación. Un ejemplo de pseudocódigo se presenta a continuación: Inicio “Cálculo del área de un triángulo” Lee base y altura Si base y altura son positivas, entonces Calcular área del triangulo = (base x altura) ÷ 2 Imprimir el área del triángulo De lo contrario Imprimir “Error, los datos deben ser positivos” Fin
Diagramas de flujo.Permite mostrar un algoritmo en forma gráfica. Para realizar esto, se cuenta con: una serie de símbolos o figuras que reemplazan a las palabras claves del pseudocódigo, flechas que indican la dirección del flujo del algoritmo y conectores. Algunos de estos símbolos son: Indica: leer, Indica: imprimir, Indica: proceso o cálculo, Indica: si… entonces… de lo contrario…, Indica: inicio o fin, etc.
Entonces, el algoritmo que calcula el área de un triángulo se puede presentar mediante un diagrama de flujo de la siguiente manera:
28
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Base, altura no
Base y altura son
si
ERROR: los datos deben ser positivos área
Fin
Lenguajes de programación Un lenguaje de programación es un conjunto de vocablos o símbolos que permiten expresar un algoritmo en el computador. Como hemos podido apreciar en el capítulo anterior a nterior el computador es una máquina que por medio de elementos electromagnéticos almacena y procesa información. En sus inicios, la manera de expresar un algoritmo en el computador era la de proporcionarle códigos binarios directamente, el lector se podrá imaginar lo complicado que sería esta tarea, en otras palabras el lenguaje que se empleaba era el de la propia máquina. La necesidad de hacer esta tarea más sencilla hizo que a alguien se le ocurriera la idea de escribir un programa que pudiera traducir, al lenguaje que entiende la máquina, otros códigos que se acerquen más al lenguaje empleado por las personas. El conjunto de códigos empleados y las reglas en que estos códigos se relacionan dan lugar a los lenguajes de programación. Clasificación de los lenguajes de programación En la actualidad existen, y han existido, innumerables lenguajes de programación, por eso, para poder entenderlos mejor debemos agruparlos en función a características comunes que puedan tener entre ellos. Existen varias maneras de clasificarlos, a continuación veremos algunas de ellas. En función de cuán cercano esté el lenguaje a la forma cómo el computador procesa la información o cuán alejado esté de ella y cercano esté al lenguaje humano, los lenguajes de programación se clasifican en: Lenguajes máquina: es el lenguaje empleado por el computador, el lenguaje está basado en códigos binarios. Si es cierto que esté lenguaje no se emplea en la actualidad para 29
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
desarrollar programas, cualquier lenguaje de programación que empleemos deberá ser traducido al lenguaje máquina para poder ser ejecutado por el computador. Como veremos más adelante estos traductores reciben los nombres de compiladores o intérpretes. Lenguajes ensambladores: (Assembler): es un lenguaje que trata de reemplazar los códigos binarios por una palabra mnemotécnica que sea más fácil de recordar. Una instrucción en lenguaje máquina podría ser como se muestra a continuación: 0100 0110 0011 1010 0001 0100 Está instrucción podría significar “mover el valor de 20 a la posición de memoria 3Ah". En lenguaje ensamblador la misma instrucción sería escrita de la siguiente manera: mov 3Ah, 20 Como se puede apreciar el código binario 0100 0110 ha sido reemplazado por la palabra “mov”, el código 0011 1010 por el valor hexadecimal 3A (la h le indica al compilador que es un valor en base 16) y finalmente el código 0010 0100 por el número 20. Indudablemente, si se quiere recordar cómo se debe hacer para colocar un valor en una posición de memoria, el emplear un lenguaje ensamblador es más fácil que emplear el lenguaje máquina. Si es cierto que el esfuerzo al crear el lenguaje ensamblador trajo muchos beneficios para programar computadores, esto no fue suficiente debido a que se sigue escribiendo programas en función de la forma que el computador trabaja, cuando se trata de programas grandes, recordar que la base del triángulo que necesitamos para calcular el área de éste se encuentra en la posición de memoria 3A h y que la altura está en la posición F4h, y que luego de calcular el área, ésta debe ser almacenad en la posición 9Bh para que la podamos utilizar más tarde, puede darnos más de un dolor de cabeza. Lenguajes de alto nivel: estos lenguajes tienen como característica principal que los códigos que emplea se acercan más al lenguaje que emplean los seres humanos para comunicarse. Para poder realizar esta tarea, los traductores realizan operaciones mucho más complejas que en los lenguajes ensambladores en donde sólo se limitan a reemplazar el código por un valor binario. En los compiladores de lenguajes de alto nivel un código puede ser remplazado por muchas instrucciones del lenguaje máquina. Una de las novedades que trajo estos lenguajes fue el empleo de “ identificadores”, la posibilidad de dar un nombre a las posiciones de memoria de modo que las podamos identificar por medio de ellos. Así por ejemplo si queremos almacenar el valor de la base de un triángulo para luego poder calcular su área, podemos emplear los identificadores “base”, “baseTri”, “baseDelTriangulo” o el que mejor nos parezca. La orden para asignar un valor al identificador podría ser como: “baseTri = 20”. Como se puede imaginar, la aparición de los lenguajes de alto nivel hizo que la programación de computadoras fuera más accesible, los programas empezaron a ser más ambiciosos y más extensos lo que obligó a mejorarlos cada vez más hasta lo que son en la actualidad. 30
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Otra manera de clasificar los lenguajes de programación es por la metodología que se emplea para diseñar y construir un programa (paradigma de programación). En este sentido podemos clasificar los lenguajes de la siguiente manera: Lenguajes imperativos o procedurales: este tipo de lenguajes de programación están muy influenciados por la forma en que el computador trabaja, los programas que en ellos se pueden implementar se basan en la idea de instrucciones que se tienen que llevar a cabo una tras otra como una receta. Estos lenguajes se basan en la definición de variables para almacenar valores, de instrucciones que permiten asignar valores a las variables y del uso de instrucciones iterativas. Ejemplos de leguajes imperativos tenemos al Assembler, Fortran, Cobol, Pascal, C, Modula2, etc. Lenguajes lógicos o declarativos: Su diseño está muy poco influido por la forma particular que trabaja el computador. En estos programas se emplean elementos basados en la lógica de predicados. Entre los lenguajes de programación más conocidos esta el Prolog. La programación lógica trata con relaciones entre objetos. Las relaciones se especifican con hechos y con reglas. Un hecho es una frase como “Pedro es padre de Juan”, “Pedro es padre de Carlos”, “Alfredo es padre de Pedro” (en Prolog, estos hechos se escribirían como: padre(Pedro, Juan), padre(Pedro, Carlos), padre(Alfredo, Pedro). Las reglas se emplean para expresar frases como “Una persona X es abuelo de otra Y si X es padre de una persona Z y Z es padre de Y” (en Prolog se escribiría como: abuelo(X, Y) :- padre(X, Z), padre(Z, Y)) o “Dos personas son hermanos si son diferentes personas y ambos tienen el mismo padre” (en Prolog, hermanos(X, Y):- X<>Y, padre(Z, X), padre(Z, Y)). La ejecución de programas lógicos consiste en la demostración de hechos sobre las relaciones por medio de preguntas, por ejemplo “¿Es Alfredo padre de Carlos?”, “¿Es Pedro padre de Carlos?”, “¿De quién es abuelo Alfredo?” (en Prolog, ?- padre(Alfredo, Carlos) cuya respuesta es falso, ?- padre(Pedro, Carlos) que se obtiene como resultado verdadero, ?- abuelo(Alfredo, X) cuya respuesta es Juan, Carlos.) Lenguajes funcionales: estos lenguajes están diseñados bajo el concepto matemático de “función”. Una función es un elemento que recibe una serie de datos y devuelve un resultado, la esencia de la programación funcional es combinar funciones para producir otras funciones más potentes. Un ejemplo de este tipo de lenguajes es el Lisp. Una función en Lisp está compuesta por una serie de datos encerrados entre paréntesis, el primero de los cuales corresponde al nombre de la función, por ejemplo “(> 3 4)” es la función mayor que evalúa el valor 3 y 4, la función devuelve verdadero o falso (en este caso falso) otro ejemplo sería (if A B C) es la función “if”, devuelve B si A es verdadero o C en caso contrario. Estas dos funciones se pueden juntar para formar otra más compleja, por ejemplo (defun mayor (n, m) (if (> n m) n m) ) ), define una nueva función denominada mayor que devuelve el valor mayor entre dos números. Lenguajes orientados a objetos: antes que apareciera este estilo de programación los lenguajes tenían la particularidad de separar, en un programa, los datos de las funcionalidades, por un lado se colocaban las definiciones de datos y por otra la funciones y procedimientos que permiten modificar el estado o el valor de los datos. Por 31
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
ejemplo si se quería escribir un programa que almacene un texto o cadena de caracteres y luego se desea borrar parte de él, se tenía que definir una variable del tipo de datos correspondiente y luego implementar una función o procedimiento que permita hacer la tarea o emplear una ya implementada en el entorno de desarrollo, para poder realizar esta acción se tiene que pasar como parámetro la variable que contiene al texto y otra información que se requiera. En lenguaje Pascal las instrucciones para realizar esta tarea serían como sigue: var texto: string; begin texto:=’Hola amigos’;
delete(texto,2, 6); …
{borrar de la variable texto 6 caracteres a partir de la posición 2} {el texto resultante queda en ‘Hogos’}
Este estilo de programación se empezó a cuestionar mucho a fines de los años 80, la críticas se basaban en que, en la naturaleza, los objetos no están separados, las partes de las funciones que realizan. Imagine un automóvil, no se puede pensar en un auto viendo por un lado un conjunto suelto de ruedan, piezas de motor asientos, etc. y por otro lado pensar en él como una máquina que nos puede desplazar, que rinde un determinado número de kilómetros por galón de gasolina, etc., cuando se piensa en un automóvil se ve como una unidad los elementos que lo conforman y las funciones que puede realizar. En este sentido la programación orientada a objetos parte de la premisa que los datos y sus funcionalidades deben estar juntos en un elemento que se le denomina “objeto”. Un programa orientado a objetos puede presentar más o menos como a continuación se muestra: string texto(‘Hola amigos’);
texto.delete(2, 6);
{ Se define la variable texto como una cadena de caracteres, asignándole el texto correspondiente} { borrar de la variable texto 6 caracteres a partir de la posición 2}
Además de estas características, los lenguajes orientados a objetos introducen conceptos como “herencia” y “polimorfismo” que permiten aumentar la versatilidad de los lenguajes de programación. Finalmente existe otra forma de clasificar los lenguajes de programación es agrupándolos por generaciones. Esta clasificación se realiza de la siguiente manera: La primera generación: la constituyen los lenguajes que aparecieron en los inicios del computador, estamos hablando del lenguaje máquina y del lenguaje ensamblador La segunda generación: se forma con la aparición de los primeros lenguajes de alto nivel a fines de los años 50. Estos lenguajes fueron el Fortran (que deriva del inglés; “Formula Translator” o traductor de fórmulas) y el Cobol (“Common Business Oriented Language”), el primero fue creado con fines científicos, era capaz de calcular muy rápidamente fórmulas complejas para la época, el segundo se hizo con fines más comerciales, se trataba de un lenguaje de programación que haciendo operaciones muy sencillas permitía manejar grandes cantidades de datos. 32
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
La tercera generación: a mediados de los años 60 se presenta una crisis en el software producto de la necesidad de crear programas más ambiciosos, en esa época la programación era muy desordenada, era muy difícil dar manteniendo a los programas por lo enredado de los programas. Es entonces que aparece la programación estructurada, concepto que se estudiará en los capítulos siguientes y que dio una orientación distinta a la forma de programas. Es por esta época que aparecen lenguajes de programación como el Algol68, el Pascal, el Modula, el C, etc. Estos lenguajes son en principio imperativos pero aplicaban los lineamientos de la programación estructurada. Lenguajes de cuarta generación: los lenguajes de esta generación se orientaron a la gestión y al tratamiento de grandes bases de datos. Son lenguajes denominados “de consulta”, esto quiere decir que con instrucciones sencillas se puede clasificar, buscar información y dar formato a la información contenida en archivos. entre estos lenguajes encontramos al Mantis, Ideal, QBE, SQL,etc. Lenguajes de quinta generación: esta generación de lenguajes se relaciona al campo de la inteligencia artificial, los sistemas basados en el conocimiento, sistemas expertos, etc. los lenguajes que a parecen en esta generación son Lisp y el Prolog.
33
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 3: Ciclo de vida de un proceso de desarrollo de software Pasos involucrados en la solución de problemas en computación “PIENSE PRIMERO, CODIFIQUE DESPUÉS” Henry Ledgard, en su libro Programming Proverbs (Hayden Book Co. Inc., 1975), enunció un interesante corolario a la Ley de Murphy al que denominó la Ley de Murphy de la programación. Enunciada en términos sencillos, la ley establece: “Entre más pronto comience a codificar un programa, mayor será el tiempo que le llevará”. Aunque la veracidad de esta máxima no ha sido todavía probada formalmente, la experiencia personal ha más que demostrado su validez. El tratar de escribir, aun el más simple de los programas, sin un esquema de solución bien definido es como construir una casa con un martillo clavos y madera, pero sin un plano. El caos se presentará casi de inmediato. Piense primero, piense después, piense un poco más y sólo entonces comience a escribir su programa. Tomado de: Introducción a la programación y a la solución de problemas con Pascal. De G.M. Scneider, S.W. Weingart, D. M. Perlman
Crear un programa en el computador no es una tarea sencilla, sin embargo si pretendemos solucionar un problema en computación sin mantener un orden y sin respetar ciertas recomendaciones, esta tarea se torna aun más difícil, y nos llevará sólo a frustraciones y fracasos. A continuación se presentan una serie de pasos que pueden ayudar en esta tarea. 1° Definición del problema Esta etapa implica entender el problema que se nos presenta. Se debe tener muy claro cuál es el resultado que se espera y con qué herramientas o datos contamos. Esto parece muy trivial pero entienda que no se puede solucionar un problema si no se sabe o entiende exactamente lo que se nos pide. Entienda que es muy diferente que se nos pida “buscar el teléfono de una persona en una guía telefónica impresa” que “buscar a quién pertenece un número telefónico en una guía telefónica impresa” y aun más diferente “buscar a quién pertenece un número telefónico en una guía telefónica digital”. El entender uno por otro, nos llevaría a una solución incorrecta o a la realización de un proceso altamente ineficiente. 2° Análisis del problema En esta etapa se hace un bosquejo de la solución del problema, aquí debe quedar claro que por simple que sea el problema, éste puede descomponerse en partes. La labor que debemos hacer, por lo tanto en esta etapa es la de identificar en forma general y no en forma específica, cuáles son las tareas que se deben realizar para solucionar un problema. Si nos plantearan el siguiente problema: “Emitir un reporte en el que aparezcan los alumnos de un curso de acuerdo con el promedio obtenido, de mayor a menos nota”, quizá la primera sensación que tengamos al leer el problema y saber que lo tenemos que resolverlo se parezca mucho a la figura siguiente: 34
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Problema
Al parecer el problema es cómo esta roca, tan complejo o grande que quizá no lo podamos solucionar o detener, y nos pasará por encima aplastándonos. Sin embargo si luego de un análisis comprendemos que el problema es un conjunto de tareas, la situación puede cambiar, esto debido a que cada tarea puede ser considerada como un problema independiente y por lo tanto estamos ahora ya no ante un problema muy grande , sino ante varios problemas más sencillos. 1 5
Tareas: 1- Obtener los datos 2- Separar a los alumnos que llevan el curso 3- Obtener los promedios de cada alumno 4- Ordenar a los alumnos por promedio 5- Imprimir los datos ordenados
2 3
4
5 4 3 2 1
Si la misma técnica se aplicase a cada tarea se podría descomponer el problema en partes mucho más pequeñas o simples que finalmente nos permita resolver el problema inicial. 3° Diseño de las estructuras de datos y del algoritmo
35
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
En esta etapa se definen los tipos de datos que se emplearán para solucionar las tareas del problema, así como también se debe describir paso a paso la solución de cada una de las tareas. La mala elección del tipo de datos que emplearemos puede llevarnos a una solución ineficiente de la tarea que queremos solucionar. Tomemos el siguiente ejemplo: “ Se tienen que asignar a los diferentes cursos las aulas en las que se dictarán las clases, para lo cual se cuenta con una listas de cursos con su respectivo número de alumnos matriculados, por otro lado se cuenta con las características del edifico de aulas, esto es la cantidad de pisos del edificio, la cantidad de aulas en cada piso y la capacidad de cada aula”, la siguiente figura ilustra el problema: Lista de cursos INF144-H431 INF144-H341 MAT149-H401 MAT149_H402 MAT149-H103 …
58 45 79 123 89
Piso …
Edificio
Piso 3 Piso 2
Aula 101 Aula 102 Aula … Cap.:40 Cap.:50 Cap.:…
Piso 1
Veamos a continuación cómo una mala elección en las estructuras de datos nos lleva a un mal algoritmo de solución: La lista de curso se puede almacenar en dos arreglos unidimensionales, uno que contiene el curso y otro la cantidad de alumnos matriculados. Con esta estructura de datos podríamos ordenar los cursos por la cantidad de alumnos, empezando por el mayor, de esa forma damos prioridad al curso que tenga más alumnos. El cuanto a los datos del edificio, la forma del mismo nos pude llevar a decidir emplear un arreglo de dos dimensiones, en donde las filas del mismo representan los pisos del edificio y las columnas el número del aula, de modo que por ejemplo la celda que se encuentra en la fila 2, columna 3 representa el aula 203; las celdas contienen la capacidad del aula. Luego como en un arreglo de dos dimensiones, la cantidad de columnas debe ser igual en todas las filas, pero el edifico no necesariamente tiene la misma cantidad de aulas por piso, se debe definir un criterio para que cuando se analicen las aulas no se tome un aula inexistente. Esto se puede hacer de varias formas, se podría colocar un cero en las celdas correspondientes a las aulas inexistentes, también se podría definir un arreglo auxiliar unidimensional en el que en el primer elemento se coloque la cantidad de aulas del piso 1, en la segunda la cantidad de aulas del piso 2 y así sucesivamente. Bajo esas condiciones, la labor de asignar un aula a un curso se centra en tomar el primer curso y empezar a buscar el en arreglo bidimensional un aula que pueda contener el número de alumnos matriculados, así se toma el elemento de la fila 1, columna 1 y se compara con el número de alumnos matriculados, si no lo puede contener se analiza el electo de la fila 1 columna 2, si no se le puede asignar se 36
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
sigue con la celda siguiente, si encontramos un aula con capacidad 0 ó si el número de columna es mayor que el valor de la primera celda del arreglo auxiliar, seguimos buscando en la fila 2, y así sucesivamente hasta encontrar un aula que soporte al curso o hasta que se terminen las aulas. Si se da lo segundo se tendrá que enviar un mensaje indicando que al curso no se le puede asignar un aula. Si se encuentra un aula para el curso el mensaje debe indicar el aula asignada, además se debe realizar una acción que impida que el aula se vuelva a asignar a otro curso. Esto se puede hacer colocando un valor de control en la celda del aula, no debe ser cero porque ese valor indica que ya no ya más aulas en la fila, quizá un valor negativo. Esta solución puede parecer lógica y acertada, sin embargo póngase a pensar que si se tiene un curso cuyo número de alumnos sobrepasa la máxima capacidad de las aulas que se tienen disponibles en ese momento, no se podrá enviar el mensaje correspondiente hasta no haber recorrido todo el arreglo bidimensional. Por otro lado cuando se busque un aula se debe hacer consultando celda por celda, inclusive se debe consultar en aquellas aulas ya asignadas, porque no hay forma de saber si ya se asigno o no sin consultarla. Esta forma de resolver el problema es muy ineficiente porque la estructura de datos elegida nos está llevando a lo que se denomina una “búsqueda secuencial”. Esta solución es equivalente a que si queremos buscar al Sr. Juan Pérez en la guía telefónica empecemos a leer uno por uno los nombres que se encuentran en la guía comenzando de la primera página. Cambiemos la estructura de datos empleada, en este caso no nos guiaremos de la forma presentada en la figura. Se mantendrán los arreglos unidimensionales que contiene a los cursos, pero en cuanto a las aulas, esta vez se elegirán dos arreglos unidimensionales; en un arreglo se colocarán los nombres de las aulas, por ejemplo en la celda 1 se colocará “101”, en la celda 2 se colocará “102”, y así sucesivamente. En el segunda arreglo la capacidad del aula, por ejemplo en la celda 1 se colocará 40, en la celda 2 se colocará 50, etc., teniendo en cuenta que el aula 101 tiene una capacidad de 40 alumnos, el aula 102 una capacidad de 50, etc. Bajo estas condiciones, para solucionar el problema, se ordenan los arreglos de cursos de mayor a menor por cantidad de alumnos matriculados, luego se ordenan los arreglos de aulas de mayor a menor capacidad. Luego se toma el primer curso y la primera aula, si a capacidad del aula es menor que la cantidad de alumnos del curso se envía un mensaje indicando que al curso no se le puede asignar un aula y no se analiza el resto de aulas, esto porque a estar ordenado el primer elemento del arreglo de aulas contiene el aula de mayor capacidad, si la capacidad del aula puede contener al curso se envía el mensaje correspondiente. Luego se toma el siguiente curso y se hace la misma operación. Como puede apreciar ambos arreglos se recorrerán una sola vez. Este es un ejemplo muy claro que muestra lo importante de las estructuras de datos en la solución de problemas. 4° Transformación del algoritmo en un programa (Codificación) 37
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Esta etapa debe empezarse definiendo el lenguaje de programación que emplearemos para codificar el programa. No se puede pensar que todos los problemas se deben o pueden resolver empleando el mismo lenguaje de programación ya que existen innumerables lenguajes, unos más eficientes en la solución de cierto tipo de problemas que otros. El no seguir esta regla implicará que la solución que demos estará influenciada por las limitaciones del lenguaje en lugar de la eficiencia en la solución, podemos llegar a situaciones como las descritas en el problema de las aulas. Esta etapa, para realizarse correctamente, implica el conocimiento y el dominio de diversos lenguajes de programación. Una vez determinado el lenguaje de programación la tarea de codificación se torna en una tarea muy sencilla de realizar, tenga en cuenta que se domina el lenguaje elegido y por otra parte la solución del problema ya está hecha en las etapas previas. 5° Depuración, prueba y validación Una vez escrito el programa, éste debe ser traducido al lenguaje máquina empleando un compilador, el compilador verificará que el código escrito en el paso anterior está correcto, si se detecta algún error se debe corregir, el compilador no traducirá el programa si existe algún error, a este tipo de error se denomina “Error de sintaxis”. Una vez que el programa esté traducido, se debe probar, esta tarea implica la ejecución del programa, el suministro de datos al programa y la verificación que los resultados sean los esperados. Si resultará que el programa no da resultados correctos, en este caso se denomina “Error lógico”, se debe verificar el código, entiéndase que la orden “Suma = A – B” puede ser sintácticamente correcta pero lógicamente incorrecta ya que queremos sumar A y B, y no restarlos. Si luego de revisarlo los resultados incorrectos persisten, se debe revisar el algoritmo empleado. La validación del programa también puede implicar medir su eficiencia, muchas veces no es suficiente que un programa nos de los resultados correctos, sino que debe hacerlo en un tiempo razonable. Si tenemos un programa al que le proporcionamos el nombre de una persona y éste nos da su número telefónico, estará bien, pero si la respuesta nos la da luego de 10 minutos, mejor lo buscamos nosotros mismos en la guía telefónica en vez de emplear el programa. El tiempo de respuesta de un programa es un factor que se debe tomar en cuenta en la mayoría de soluciones planteadas. 6° Documentación Una vez concluido, el programa debe ser entregado al usuario que lo va a manejar. Esta persona debe ser entrenada para que pueda utilizar el programa sin problemas, para esto se debe preparar un documento denominado “Manual de usuario” en el que se explique cómo poner en marcha el programa así como la manera de acceder a todas las opciones que permite el programa. 38
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Por otro lado se debe pensar que en algún momento se puede desear modificar o actualizar el programa, por lo que se va a requerir las especificaciones que se emplearon para resolver el problema. Es por esta razón que se debe elaborar otro documento denominado “Manual técnico” donde queden escritos estos detalles.
Traductores de lenguajes de programación: Compiladores e Intérpretes Para escribir un programa empleando un lenguaje de programación se requiere en primer lugar un procesador de palabras, un programa que nos permita escribir un texto y almacenarlo en el computador. Sin embargo, este texto no puede ser ejecutado en el computador debido a que para él sólo son códigos (letras y símbolos) almacenados. Para que este texto pueda ser ejecutado por el computador, las órdenes escritas en este texto deben ser traducidas al lenguaje que entiende el computador, el “lenguaje máquina”. Para poder traducir este texto se requiere de de otro programa que pueda realizar esta tarea, estos traductores reciben los nombres de “compiladores” o “intérpretes”. Cada lenguaje de programación que empleemos debe tener su propio traductor. En el mercado existen muchos productos que reúnen en un mismo entorno procesadores de palabras, traductores y otros programas que permiten depurar los programas, esto se hace con la finalidad de facilitar la labor del programador. Estos productos reciben la denominación de IDE (Integrated Development Environment) o “Entorno de Desarrollo integrado”. Un ejemplo de estos productos son el Borland™ Pascal, el Visual Studio™, etc. Compiladores Un compilador, como ya hemos indicado es un programa cuya tarea es la de traducir un texto escrito según las reglas de un lenguaje de programación al lenguaje máquina, para esto toma como dato el archivo con el texto, denominado “Programa fuente” y crea otro archivo con los códigos en lenguaje máquina denominado “programa objeto”. El compilador concluye su tarea cuando todo el texto haya sido traducido, esto quiere decir que si en el proceso de traducción encontrara un error de sintaxis el compilador se detiene, envía un mensaje indicando el error y no crea el programa objeto. Intérpretes Un intérprete también es un traductor, sin embargo la forma en que realiza su tarea es diferente a la del compilador. El intérprete no crea un archivo como el programa objeto, lo que hace es tomar del archivo fuente una a una las órdenes las traduce e inmediatamente las ejecuta. Esto permite apreciar la ejecución de un programa aun si tuviera errores de sintaxis, mientras el intérprete no llegue a la orden con el error, el programa continúa su ejecución. Esto puede ser una ventaja, sin embargo se requerirá del intérprete cada vez que se desee ejecutar el programa; un programa que ha sido compilado no requiera del compilador para ser ejecutado ya que el programa objeto se 39
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
encuentra escrito en lenguaje máquina y por lo tanto el computador lo entiende completamente. Fases de la compilación Para conseguir un programa que se ejecute directamente por el computador se requiere, como hemos dicho, un compilador. Se pueden definir dos etapas en el proceso de compilación, la compilación propiamente dicha y el enlace (link). La fase de compilación propiamente dicha es la etapa de traducción, el texto del programa fuente se traduce al lenguaje máquina, es aquí donde se detectan los errores de sintaxis. Al finalizar esta etapa el programa no puede ejecutarse aun, esto se debe a que, entre otras cosas, las órdenes muchas veces emplean elementos que no pueden ser ejecutadas directamente, por ejemplo si se pretende calcular el seno de un valor x y asignarlo a una variable A, la orden dice: A = sin(x); La traducción de esta línea lleva a indicar que se debe calcular el seno de x, sin embargo no indica cómo se debe calcular el seno, y se debe entender que la computadora no tiene porqué saberlo. Las instrucciones para calcular el seno se le deben proporcionar al computador, sin embargo esas órdenes no se escriben el programa fuente, imagínese lo complicado y laborioso que sería si tuviéramos que hacerlo. Es en la fase del enlace donde se incorporarán las órdenes necesarias para completar el programa objeto. Resulta que los entornos de desarrollo incluyen una lista de archivos donde se encuentran precisamente las órdenes para poder calcular por ejemplo el seno de un ángulo. Estos archivos por lo general ya están en lenguaje máquina por lo que no requieren una traducción, simplemente que se los incorpore al programa objeto. Esta etapa de la compilación permite también que podamos incorporar al programa otros archivos que hayan sido creados por el mismo programador o por otros programadores. Resulta que, como vimos en la etapa de análisis, un problema debe dividirse en partes más sencilla, cada parte puede ser analizada por diferentes personas que forman parte de un equipo, cada persona puede elaborar la solución y el código de cada parte, luego aplicar la fase de compilación propiamente dicha a cada uno de sus módulos y cuando estos estén traducidos enlazarlos en un único programa fuente. Esto implica que un problema puede ser atacado en paralelo por diferentes personas obteniéndose una respuesta más rápida a la solución del problema, incluso como lo que se enlaza son códigos en lenguaje máquina, cada módulo pudo haber sido elaborado en diferentes lenguajes de programación, el que más se adapte a la solución planteada en el respectivo módulo.
40
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 4: Estructura general de un programa Definición de programa Como se dijo en capítulos anteriores, un lenguaje de programación es un conjunto de vocablos o símbolos que permiten expresar un algoritmo en el computador. Para poder empezar a desarrollar programas se debe conocer al detalle el o los lenguajes de programación que emplearemos para este fin. Cuanto más conozcamos de un lenguaje podremos conseguir mejores y más eficientes programas. A continuación estudiaremos las partes más elementales de un lenguaje de programación Concepto de Identificador Como se mencionó en capítulos anteriores, la memoria del computador está compuesta por una serie de celdas que sirven para almacenar información, cada una de estas celdas se identifica por un número al que se denomina “dirección de memoria”. Si cada vez que en un programa tuviéramos que guardar un dato, que luego vamos a utilizar para realizar alguna operación en el programa, o quisiéramos ejecutar una porción de código ubicado en alguna parte de la memoria, tuviéramos que hacerlo escribiendo la dirección de memoria; elaborar un programa sería algo muy complicado. Afortunadamente los lenguajes de programación de alto nivel proporcionan una herramienta que permite que esta tarea sea muy simple, esta herramienta se denomina Identificador. Un identificador es un nombre que está relacionado con una dirección de memoria y sirve para denotar ciertos elementos de un programa. Estos elementos son: las variables, las constantes, las funciones y los procedimientos, elementos que se definirán más adelante. Los identificadores son definidos por el programador, esto quiere decir que si éste requiere de una variable o una función, pues sólo tiene que pensar en un nombre para identificarla y declararla en el programa. Luego, si quiere hacer uso de esa variable o función, sólo tiene que hacerlo a través del nombre que le dio. Un identificador no puede ser definido de cualquier manera, los lenguajes de programación tiene una serie de reglas de formación de identificadores. A continuación se describen algunas de ellas. En el leguaje Pascal, los identificadores sólo pueden estar formados por las letras del alfabeto inglés (a, b, c,… x, y, z ó A, B, C,… X, Y, Z), los dígitos decimales (0, 1, 2,… 7, 8, 9) y el signo de subrayar, para formar un identificador sólo tenemos que definir una palabra combinando estos símbolos, sin embargo un identificador no puede empezar con un dígito. Las palabras que empleemos para definir un identificador no puede coincidir con una palabra reservada (ver tabla 4.1 – Palabras reservadas del Pascal). El compilador de Pascal no hace distinción entre mayúsculas y minúsculas, esto quiere decir que para él da lo mismo definir el identificador suma, que Suma o que SuMa, o SUMa, o SUMA, etc. De acuerdo a esto, será considerada como correcta, para la formación de identificadores, la lista siguiente: 41
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
suma Capacidad apellidoPat INTERES tasa_de_interes Factor398 _AX _678 _456_Base _myor_ La siguiente lista, rompe las reglas de formación, por lo que no son aceptadas por el compilador: Año (la ñ no pertenece al alfabeto inglés) Interés (la é no pertenece al alfabeto inglés) δx (δ no es un símbolo aceptado) gastos+intereses (+ no es un símbolo aceptado) Apellido Paterno (no se aceptan identificadores con espacios) 456_Base (no se puede empezar con un dígito) REPEAT (repeat es una palabra reservada) and case div end function in label not packed repeat string unit while
asm const do exports goto inherited library object procedure set then until with
array constructor downto file if inline mod of program shl to uses xor
begin destructor else for implementation interface nil or record shr type var
Tabla 4.1 – Palabras reservadas del Pascal
En el lenguaje C y C++, las reglas son similares, con la única diferencia que aquí si se distinguen entre mayúsculas y minúsculas, esto quiere decir que para el compilador suma y SUMA serán dos identificadores diferentes y estarán relacionados a distintos elementos.
Partes constitutivas de un programa: 1° Encabezado: Es una parte opcional en todo programa, la finalidad del encabezado es la de poder indicar, de una manera sencilla: el propósito del programa, el autor del programa, la fecha en que se creó, la fecha que se hizo la última modificación, etc., de modo que una persona que lea el programa tenga esa información en las primeras líneas del programa y no tenga que buscarla a través del texto del programa o por otros medios. Por lo general es muy similar la forma que se coloca un encabezado en un programa, por ejemplo en el lenguaje C o C++, el encabezado se coloca a través de textos que son ignorados por el compilador, denominados comentarios. Un comentario en lenguaje C y C++ se coloca luego de los símbolos /* y se da por concluido con los símbolos */. Por ejemplo: 42
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
/* Programa que permite calcular las raíces de una ecuación de segundo grado. Autor: Ana Cecilia Roncal Neyra Fecha de creación: 01/01/2003 Última actualización: 10/03/2006 */
En el lenguaje C++ también se pueden emplear lo que se denomina comentarios en línea, los cuales se delimitan por los símbolos // y el cambio de línea, de la siguiente manera: // // // //
Programa que permite calcular las raíces de una ecuación de segundo grado. Autor: Ana Cecilia Roncal Neyra Fecha de creación: 01/01/2003 Última actualización: 10/03/2006
En el lenguaje Pascal se combina una instrucción y comentarios, la instrucción empieza con la palabra reservada program seguida de una palabra que debe ceñirse a las reglas de formación de un identificador. Los comentarios se encierran ente los símbolos { y }. Por ejemplo: program permiteCalcularLasRaicesDeUnaEcuacionDeSegundoGrado; {
Autor: Ana Cecilia Roncal Neyra Fecha de creación: 01/01/2003 Última actualización: 10/03/2006 }
2° Inclusión de bibliotecas de funciones y subprogramas: Los compiladores de los diferentes lenguajes de programación implementan de manera nativa, dentro de los elementos que conforman el lenguaje, una serie de palabras reservadas, funciones y subprogramas de uso común que pueden ser utilizados en cualquier parte del programa. Sin embargo, para mayor comodidad de los usuarios, con la finalidad que cada vez que escriban un programa no se tengan que empezarlo de cero, los diferentes compiladores definen mecanismos que permiten incorporar a los programas que estamos escribiendo, bibliotecas de de funciones y subprogramas, es decir archivos que contengan el código de funciones y subprogramas, de manera que podamos reutilizar el código. El lenguaje C y C++ definen en su núcleo una cantidad muy reducida de palabras reservadas y todos los demás elementos se incorporan a través de bibliotecas de funciones, en algunos casos estas bibliotecas son proporcionadas por el entorno de desarrollo. Por ejemplo, si se desea realizar una operación de ingreso de datos, se debe incorporar la biblioteca de entrada y salida. El proceso de incorporación de una biblioteca en C ó C++ se realiza en dos pasos, el primero es incorporar la declaración de la funciones en el texto del programa que estamos escribiendo, esto se hace mediante una cláusula denominada include y el segundo paso es incorporar el código al programa en el proceso de compilación, este paso depende del entorno de desarrollo que estemos utilizando. A continuación se presenta un ejemplo en C:
43
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo Aquí se incluyen las declaraciones de las funciones que se encuentran en la biblioteca de funciones para graficar denominada winbgim Estos son elementos propios del núcleo de C
#include "winbgim.h"
int main( ) { initwindow(400,300); moveto(0,0); lineto(50,50); closegraph(); return 0;
Esta son funciones implementadas en la biblioteca para graficar, denominada winbgim. Con estas funciones se crea y muestra una ventana gráfica de 400x300 píxeles, se mueve el cursor de dibujo a la posición 0,0, se dibuja una línea hasta la posición 50,50 y finalmente se cierra la ventana gráfica. No hay que escribir el código para realizar estas tareas, sólo invocar a las funciones.
}
Estos son elementos propios del núcleo de C
Luego el código de la biblioteca se incorpora al programa ejecutable con operaciones desde la línea de comandos del sistema operativo, como la que se muestra a continuación: bcc32 -edibujo.exe dibujo.cpp winbgim.lib
Orden para compilar Nombre del programa ejecutable que se creará
Biblioteca de funciones para graficar Archivo con el texto del programa anterior
En el lenguaje Pascal, la inclusión de bibliotecas de funciones se hace a través de lo que denominan Unidades (UNIT). Una unidad es un archivo compuesto por una serie de funciones y procedimientos, que ya está compilado y que se incorpora al programa que estamos elaborando mediante la cláusula USES. Al igual que el lenguaje C y C++, los entornos de desarrollo creados para Pascal traen un conjunto de bibliotecas de funciones que pueden ser empleadas en el momento que se requieran, y también los usuarios pueden crear sus propias bibliotecas, sin embargo la elaboración de una unidad en Pascal es un poco más laborioso que cuando se quiere hacer lo mismo en lenguaje C y C++, ya que en Pascal se requiere seguir una serie de reglas de sintaxis definidas por el lenguaje y que son diferentes a la de un simple programa; en el lenguaje C y C++ es más simple porque no se requieren reglas especiales. A continuación se presenta un ejemplo: program ejemploDeUnidades; uses CRT; begin clrscr; writeln(‘Se borró la pantalla’); end.
Aquí se incluyen las funciones y procedimientos que se encuentran en la biblioteca de manejo de pantalla denominada CRT.tpu Este es un procedimiento de la biblioteca de manejo de pantalla. Estos son elementos propios del núcleo de Pascal
3° Definición de constantes: Una constante es un elemento de un programa definido por un identificador. En la dirección de memoria relacionada al identificador se ha colocado un valor cualquiera, este valor no podrá ser cambiado a lo largo del programa, de allí su nombre. En Pascal las constantes se definen en una zona, esto quiere decir que todas las constantes definidas en un programa van agrupadas por lo general al inicio del programa, la forma como se realiza esto se muestra a continuación: 44
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
program ejemploDeDeclatracionDeConstantes ; const IMPUEST0 = 0.12; Todas se definen con una sola cláusula: const FACTOR = 3; MAXIMO = 200;
En el lenguaje C y C++, las constantes se individualmente, esto quiere decir que para cada una se debe la cláusula correspondiente: En C:
En C++ 2:
#define IMPUESTO 0.12 #define FACTOR 9 #define MAXIMIO 200 const float IMPUESTO = 9.12; const float FACTOR = 9; const int MAXIMO = 200;
Como regla convencional, para identificar fácilmente una constante de cualquier otro elemento del programa, el identificador empleado para definir una constante debe hacerse enteramente en mayúsculas. 4° Definición de tipos de datos: En el capítulo 1, se pudo apreciar cómo se representan los datos en la memoria del computador. Cuando se habla de tipos de datos en programación, se está refiriendo a cuáles son las formas de representación de datos que maneja un lenguaje de programación y cómo se clasifican éstos. En el lenguaje Pascal, los datos se clasifican en dos tipos, los tipos escalares y los tipos estructurados. Cuando hablamos de tipos escalares, nos estamos refiriendo a datos individuales, esto quiere decir que si definimos un elemento de tipo escalar, estaremos haciendo referencia a un solo valor. Por otro lado un tipo estructurado es todo lo contrario, cuando definimos un elemento estructurado estamos hablando de un conjunto de valores. Los tipos escalares se clasifican en tipos estándar y en tipos definidos por el usuario, mientras que los datos estructurados se clasifican en cadenas de caracteres, arreglos, conjuntos, registros, archivos y punteros3. Los tipos estándar se refieren a datos cuyos valores están enmarcados en rangos definidos por el propio lenguaje, mientras que en los definidos por el usuario es el programador el que define su rango de valores. Los tipos estándar de clasifican en ordinales y no ordinales, mientras que los definidos por el usuario se clasifican en subintervalos y enumerados4 Los tipos ordinales se refieren a valores que se encuentra en un rango de datos para los que se cumple la siguiente regla: “Todo valor del conjunto, excepto el primero y el último, poseen un único valor que los precede y un único valor que los sucede”. Dentro de esta definición podemos encontrar a los valores enteros, un valor como 253 tiene un único predecesor, el 252, y un único sucesor, el 254. Los valores de tipo caracter también son ordinales, si el caracter ‘Q’ pertenece al código ASCII, tiene un único predecesor, el caracter ‘P’ y un único sucesor, el caracter ‘ R’. 2
int y float son tipos de datos. Este concepto se analizará más adelante. Los tipos estructurados se estudiarán detalladamente en los capítulos siguientes. 4 Los tipos definidos por el usuario serán analizados más adelante. 3
45
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
También podemos encontrar los tipos lógicos, al valor falso (false) lo sucede el valor verdadero (true), y a éste lo precede el valor falso. Los tipos no ordinales son aquellos que no se puede establece su predecesor ni su sucesor, este es el caso de los valores reales o de punto flotante. Al valor real como por ejemplo 32.67 no se le puede definir un sucesor, ya que si decimos que lo sucede el 32.68 estaríamos en un error ya que también podría ser 32.657 ó 32.671 ó 32.6701 ó 32.6700001, etc. La tabla 4.2 muestra un esquema de los tipos de datos definidos en Pascal. El lenguaje Pascal define una variedad de tipos enteros y reales, los cuales se diferencian por la cantidad de bytes que empelan para su representación, lo que está directamente ligado, como ya hemos dicho, con el rango de valores que pueden manejar. Las tablas 4.3 y 4.4 nos muestran estos tipos de datos. Los valores de tipo caracter y lógico, se definen con los tipos Char y Boolean respectivamente, ambos se representan en un byte. Las cadenas de caracteres se definen mediante el tipo String y se almacenan en 256 bytes. En el lenguaje C y C++ se definen tipos de datos similares. Tipos de Datos en Pascal
Tipos estructurados
Tipos escalares
Arreglos Tipos definidos por El usuario
Tipos estándar
Tipos ordinales
Enumerados
Tipos no ordinales
Sub-intervalos
Enteros
Cadenas de caracteres Registros Archivos
Reales Punteros
Caracteres Lógicos
Objetos Tabla 4.2 Tipos de datos de Pascal
Tipo de dato Byte ShortInt Word Integer LongInt
Espacio de almacenamiento 1 byte 1 byte 2 bytes 4 bytes 5
Rango de valores 0 .. 255 – valores sin signo -127 .. 28 – valores con signo 0 .. 6,5535 – valores sin signo -2,147’484,648 .. 2,147’484,647 Valores con signo 4 bytes -2,147’484,648 .. 2,147’484,647 Valores con signo Tabla 4.3 – Tipos de datos enteros en Pascal
5
Dependiendo del entorno de desarrollo que se emplee, los enteros de tipo Integer pueden definirse con 2 o 4 bytes.
46
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Tipo de dato Single Real Double Extended Comp 6
Espacio de almacenamiento 4 bytes 6 bytes 8 bytes
Rango de valores Dígitos significativos ± 1.5x10-45 .. 3.4x1038 7a8 ± 2.9x10-39 .. 1.7x1038 11 a 12 ± 5.0x10-324 .. 15 a 16 1.7x10308 10 bytes ± 1.9x10-4932 .. 19 a 20 1.1x104932 8 bytes -263 + 1 .. 263 - 1 19 a 20 Tabla 4.4 – Tipos de datos reales o de punto flotante en Pascal
Volviendo a la definición de tipos de datos, los lenguajes de programación permiten crear nuevos tipos de datos, a partir de los datos definidos en forma nativa por el lenguaje, este es el caso de de los datos estructurados. Una vez creados los nuevos tipos de datos, se pueden definir elementos de esos nuevos tipos. En el lenguaje Pascal esta operación se realiza de una manera muy simple, el nombre que se emplee para el nuevo tipo de dato debe seguir las mismas reglas de formación que los identificadores y la sintaxis empleada se muestra continuación: type TipoDeDatoEntero = Integer;
En este caso se define un nuevo tipo de dato llamado TipoDeDatoEntero, con las mismas características que los tipos de datos Integer. Más adelante se verán formas más complejas de definición de tipos de datos, cuando se vean los tipos de datos estructurados. Finalmente, como en el caso de las constantes, para poder diferenciar un tipo de dato de otro elemento, convencionalmente la palabra que define el nuevo tipo de dato debe empezar con una letra mayúscula y si ésta está formada por varias palabras, cada palabra debe empezar con una letra mayúscula como se indica en el ejemplo. 5° Definición de subprogramas: En los capítulos anteriores se vio la conveniencia de dividir un problema en partes más pequeñas parar facilitar la solución de un problema. Los subprogramas son porciones de código que nos permiten dividir un programa en varias partes. Los subprogramas, de acuerdo a cómo realizan la tarea que se les encomienda, pueden dividirse en dos tipos, las funciones y los procedimientos. Las funciones están enfocadas al cálculo o a la obtención de un valor determinado, por ejemplo si necesitamos obtener el área de un triángulo, lo más conveniente sería definir una función a la que le entreguemos la base y altura del triangulo y el código de la función calculará y entregará el área; si queremos determinar el mejor alumno de un curso, se puede elaborar una función que reciba la tabla con los nombres y notas del curso, la función buscará en la tabla el alumno que tiene la mayor nota y nos entregará su nombre. Los procedimientos están orientados a realizar procesos. Por ejemplo es útil crear un procedimiento si se desea obtener un listado en el que aparezcan los alumnos de un curso que salieron desaprobados, o si por ejemplo se desea actualizar la información contenida en un archivo. 6
Los tipos de datos Comp manejan únicamente valores enteros de 8 bytes
47
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Normalmente los subprogramas se desarrollan luego de tener una idea clara de la solución del problema, este momento por lo general se da después de haber planteado el programa principal, es por eso que en esta zona del programa sólo se colocan las definiciones de los subprogramas, dejando la implementación de los mismos para después. En lenguajes de programación como C y C++ esto se da así, sin embargo en Pascal uno se ve obligado a colocar la implementación en esta zona, lo que no quita que se pueda implementar después de haber implementado el programa principal. 6° Definición de variables: Una variable es, al igual que las constantes, un elemento de un programa definido por un identificador. Sin embargo a diferencia de estas últimas, las variables pueden recibir valores en cualquier parte del programa, pudiéndose reemplazar o modificar su valor anteriormente asignado. Una variable está relacionada directamente con un tipo de dato del lenguaje. En el lenguaje Pascal la definición de variables se realiza de acuerdo a la siguiente regla sintáctica: var
Identificador
:
Ti o de dato
;
,
Como regla convencional, también para diferenciarlas, los identificadores que se empleen como variables deben empezar con una letra minúscula. A continuación se muestran ejemplos de definición de variables en Pascal. program ejemploDeDeclatracionDeVariables ; base, altura, areaDelTriangulo : Real; var opcionInicial : Char; finDelCiclo, hayError, seTrerminaronLosDatos : Boolean; contador, linea : Integer 7° Programa principal: Es la porción de código que se ejecuta primero en un programa. Aquí se realizan las tareas más generales del programa, haciendo llamados a los subprogramas que realizarán la tarea de solucionar el problema. En C y C++ el programa principal se implementa mediante una función denominada “main”, en Pascal esto se indica mediante un bloque de instrucciones que empieza con la palabra reservada begin y termina con la palabra reservada end seguida de un punto. A continuación se muestra un programa implementado en C y en Pascal, en donde se aprecian las partes de un programa, como se podrá apreciar no necesariamente se deben colocar todas ellas. En C:
/* Programa que calcula e imprime el volumen d eun cilindro.*/ #include
// biblioteca que permita la entrada y salida de datos #define PI 3.141592 float volumen(float, float); // definición de una función
48
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
void main (void) { // función principal float radioDeLaBase, altura, vol; // declaración de variables scanf(“%f %f”, &radioDeLaBase, &altura); // lectura de datos vol = volumen(radioDeLaBase, altura); printf(“Volumen= %f\n”, vol); //impresión de resultados } float volumen(float radioDeLaBase, float altura) { // implementacion de la función return PI * radioDeLaBase * radioDeLaBase * altura; } En Pascal:
program calculoDelVolumenDeUnCilindro; const PI = 3.141592; function volumen(radioDeLaBase, altura:Real): Real; begin volumen := PI * radioDeLaBase * radioDeLaBase * altura; end; var radioDeLaBase, altura, vol: Real; begin readln(radioDeLaBase, altura); vol := volumen(radioDeLaBase, altura); writeln(‘Volumen = ‘, vol:8:2); end.
Asignación de valores y expresiones Una operación de asignación es una operación por medio de la cual se coloca información en una variable. En Pascal7 este proceso se realiza empleando la siguiente regla sintáctica: variable
:=
expresión
Podemos definir una expresión como un conjunto de valores constantes, constantes, variables y funciones, unidos por algún operador, que se agrupan con la finalidad de obtener un resultado. El resultado que se obtenga al evaluar la expresión debe ser del mismo tipo de dato que la variable, si el resultado de la expresión da por ejemplo un valor real, éste no se le podrá asignar a una variable de tipo entero sin que se produzca, dependiendo del lenguaje de programación que se emplee, un error o que se asigne un valor incorrecto a la variable. Un valor constante es la representación de información que no está contenida en una variable o constante y que se requiere en un instante dado. Esta representación depende del tipo de dato que queramos manejar. A continuación mostramos una tabla en la que se aprecian las diferentes maneras de representar valores constantes:
7
A partir de este momento los ejemplos estarán referidos al lenguaje Pascal únicamente.
49
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Enteros: Reales: Caracteres: Cadenas de caracteres: Lógicos:
Valores decimales: Valores hexadecimales: Valores decimales: Notación científica: Con el caracter ASCII: Con el código ASCII ‘Juan López’ true
37 5237 8 12345 $AB3 $F0B $C5A 3.1416 3427889.523 3.8451e+14 8.345e-25 3.5E26 ‘A’
#65
‘*’
#74
‘&’
#26
#10
‘Hola amigos como están’
false
Una operación de asignación por lo tanto se puede dar en los siguientes términos: program asignacionDeValoresConstantes; var i1, i2: Integer; r1, r2: Real; c1, c2: Char b1, b2: Boolean; cad: String; begin i1 := 37; i2 := $F0B; r1 := 3.1416; r2 := 8.345e-25; c1 := ‘A’; c2 := #10; b1 := true; b2 := false; cad := ‘Juan López’; end. Las constantes fueron definidas en la sección anterior, sin embargo aquí queremos resaltar la importancia del uso de constantes en una expresión, en lugar de emplear valores constantes. Si en un programa quisiéramos calcular el impuesto que tenemos que pagar por una venta realizada, podemos realizarlo de dos maneras diferentes: 1° impuesto := 0.18 * ventasRealizadas; ó
2°
const IGV = 0.18; begin … impuesto := IGV * ventasRealizadas; La primera forma es más sencilla que la segunda y por eso muchas veces nos veremos tentados a emplearla. Sin embargo si es cierto que a la hora de programar es más fácil hacerlo de la primera forma, cuando tengamos que depurar o mantener el programa la primera forma nos dará muchos dolores de cabeza y nos hará perder mucho tiempo. Imagínese que se optó por la primera forma y que la instrucción de es parte de un programa de contabilidad de una empresa. La cantidad de líneas que puede tener el programa pueden llegar a miles; imagínese que luego de haber escrito el programa y saber que se ejecuta correctamente, se determina que el impuesto ya no es 18% sino 17%. En ese momento se tendría que buscar la ubicación dónde se colocó la instrucción, si el cambio se hizo meses depuse de haber entregar el programa, esta tarea será muy engorrosa y nos tomará mucho tiempo. Si por otro lado el cambio se debe realizar en un tiempo muy corto y si el valor constante (0.18) aparece en otras partes del programa, la 50
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
premura nos podría hacer equivocarnos y hacer que cambiemos un valor que no era el que debíamos cambiar; esto podría ocasionar pérdidas muy grandes en la empresa, incluso problemas legales. Si por el contrario se optó por la segunda forma, el programador sólo tiene que explorar las primeras líneas del programa buscar la constante IGV cambiar su valor, volver a compilar el programa y tenerlo listo en unos pocos minutos, con la seguridad que no hayamos cometido errores. Es por esta razón que se recomienda el uso de constantes en un programa en lugar de los valores constantes. Los valores constantes deben emplearse sólo en caso de constantes universales (p. e.: π = 3.14.1592, g = 9.78, e = 2.7182,
etc.) o valores que estemos seguros no cambiarán en el tiempo. Las variables, que fueron definidas en la sección anterior, son elementos que deben ser tratados también con mucho cuidado en un programa. Como hemos visto, las variables son elementos definidos mediante un identificador, esto significa que tenemos mucha libertad para elegir el nombre que le daremos a una variable y esto es muy importante en un programa. Cuando uno escribe un programa, debe pensar que el programa tendrá que ser depurado cuando éste se termine, y cada cierto tiempo se le deberá dar mantenimiento, por eso es muy importante que el programa sea muy fácil de entender, de lo contrario se perderá mucho tiempo en estas tareas. Lo primero que debemos tomar en cuenta es que las variables deben ser muy fácilmente identificadas de los otros elementos del un programa, es por eso que hemos dicho que las variables deben empezar con una letra minúscula, las constantes deben estar enteramente escritas en letras mayúsculas y los tipos de datos deben empezar con una letra mayúscula. La segunda regla, que debe ser la más importante es que el nombre que le demos a las variables, deben estar directamente relacionados con su contenido, el siguiente ejemplo refleja esta regla. La siguiente es una operación de asignación en la que se entrega a una variable el resultado de una expresión: a := ((b + c + d + e - f) * g / 3 + h * i + j * k)/(g + i + k); La operación es, sintácticamente correcta, esto quiere decir que cuando compilemos el programa no nos va a dar errores. Por otro lado si hemos tenido el cuidado de asignar valores correctos a cada una de las variables, vamos a tener un resultado correcto y esperado. Sin embargo, alguien puede decir, sin dudar, qué es lo que se quieres calcular en esta instrucción. Usted se puede imaginar que después de un año de haber escrito esta instrucción en un programa, el creador del programa se podrá acordar qué es los que representa la variable f. Las respuestas a estas preguntas son indudablemente no. Veamos ahora la misma instrucción escrita de otra manera: promCurso := ((notaPr1 + notaPr2 + notaPr3 + notaPr4 - notaMinimaPr) * pesoPr / 3 + notaEx1 * pesoEx1 + notaEx2 * pesoEx2)/( pesoPr + pespEx1 + pesoEx2); ¿Qué queremos calcular? Pues es obvio que el promedio de un curso. ¿Qué significa notaMinimaPr? Pues en muy probable que la nota mínima de prácticas. 51
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Es cierto que la segunda expresión es más larga y nos va a tomar más tiempo en escribirla que la primera, pero el tiempo que dediquemos a depurar y mantener el programa no tendrá punto de comparación. Los operadores 8 son elementos de enlace en una expresión y nos sirven para realizar operaciones, a continuación se presentan los distintos operadores que se encuentran en el Pascal. 1° Operadores aritméticos: trabajan con valores numéricos, la tabla 4.5 muestra estos operadores: Operador + *
Operación Suma Resta Multiplicación
Tipos de datos permitidos
/
División real
enteros o reales
div mod
División entera Módulo o resto
enteros o reales
Tipo de dato del resultado entero + entero 9 » entero + real real + entero » real + real
sólo enteros sólo enteros Tabla 4.5 - Operadores aritméticos
entero real
Real Entero Entero
El código mostrado a continuación nos muestra ejemplo del uso de estos operadores: program usoDeOperadoresAritmeticos; var a, b, c: Integer; r, s, t: Real; begin a := 7; b := 25; r := 3.56; s := 36.21; c := a + b; {la variable c recibe el valor de 32} t := a * r; {la variable t recibe el valor de 24.92} c := s - b; {error: el resultado es 11.21, no puede asignarse a la variable c} t := s / r; {la variable t recibe el valor de 10.171348315} c := a / b; {error el resultado es 0.28, no puede asignarse a la variable c} c := b div a; {la variable c recibe el valor de 3} c := b mod a; {la variable c recibe el valor de 4} end.
2° Operadores de relación: trabajan con valores escalares. Como su nombre lo indica relacionan dos valores, el resultado es un valor lógico (true o false), la tabla 4.6 muestra estos operadores: Operador Operación = igualdad <> desigualdad < menor que <= menor o igual > mayor que >= mayor o igual Tabla 4.6 - Operadores de relación 8
Con la finalidad de estar en condiciones de elaborar expresiones rápidamente, se optó por tratar este tema antes que el de funciones. 9 El signo + puede reemplazarse por – ó *
52
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
A continuación mostramos ejemplos del uso de estos operadores: program usoDeOperadoresDeRelacion; var a, b: Integer; r, s: Real; m, n: Char; cad1, cad2: String; l1, l2, l3: Boolean; begin a := 7; b := 25; r := 3.56; s := 36.21; l1 := a = b; {la variable l1 recibe el valor de false} l2 : = r < s; {la variable l2 recibe el valor de true} l3 := r = 3.56; { esta operación no debe realizarse en un programa, esto debido a que por la forma como se codifican los valores reales podría asignar a l3 el valor de false} m := ‘A’; n := ‘F’; l1 := m < n; { se comparan los códigos ASCII de ambas variables, se le asigna a la variable l1 en valor de true} m := ‘a’; n := ‘F’; l1 := m < n; { el código ASCI I del caracter ‘a’ es mayor que el de ‘F’, se le asigna a la variable l1 en valor de false} cad1 := ‘maria’; cad2 := ‘mario’; l1 := cad1 < cad2; { se comparan los códigos ASCII del primer caracter de cada cadena, si son iguales se comparan los dos segundos hasta encontrar un par diferente, la respuesta se da según la comparación de estos últimos caracteres. Se le asigna a la variable l1 en valor de true} l1 := true; l2 := false; l3 := l1 <> l2; { true se almacena como 1 y false como 0, se le asigna a la variable l3 en valor de true} end. 3° Operadores de lógicos: sirven para enlazar expresiones lógicas, el resultado es un valor lógico (true o false), la tabla 4.7 muestra estos operadores: Operador Operación not negación and conjunción or disyunción inclusiva xor disyunción exclusiva Tabla 4.7 - Operadores lógicos
Estos operadores se comportan según la siguiente tabla de verdad: expL1 false false true true
expL2 false true false true
not expL1 expL1 and expL2 expL1 or expL2 expL1 xor expL2 true false false false true false true true false false true true false true true false Tabla 4.8 – Tabla de verdad de los operadores lógicos
A continuación se presentan algunos ejemplos: 53
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
program usoDeOperadoresLogicos; var a, b: Integer; r, s: Real; l1, l2, l3: Boolean; begin a := 7; b := 25; r := 3.56; s := 36.21; l1 := a = b; {la variable l1 recibe el valor de false} l2 : = (r < s) and (a >= 0); {la variable l2 recibe el valor de true} l3 := not l1 xor (s > 10.5); {la variable l3 recibe el valor de false} end. 4° Operador de caracteres: trabajan sobre cadenas de caracteres, la tabla 4.9 muestra este operador: Operador Operación + concatenación Tabla 4.9 - Operadores de caracteres
Este operador junta dos cadenas de caracteres para forma una sola. program usoDeOperadoresDeCaracteres; var cad1, cad2, cad3: String; begin cad1 := ‘Juan’ ; cad2 := ‘López’; {la variable cad3 recibe el valor de ‘JuanLópez’} cad3 := cad1 + cad2; cad3 := cad1 + ‘ ‘ + cad2; {la variable cad3 recibe el valor de ‘Juan López’} end. 5° Operadores de Bits: Sirven para manipular datos enteros desde su representación interna, esto es, las operaciones se dan a nivel de los bits de los datos. La confusión que se puede dar con estos operadores es que coinciden con las mismas palabras con las que se definen los operadores lógicos, sin embargo habrá que entender que los operadores de bits trabajan con variables enteras a diferencia de los otros que trabajan sobre expresiones lógicas. La tabla 4.10 muestra estos operadores: Operador not and or xor shl shr
Operación negación a nivel de bits conjunción a nivel de bits disyunción inclusiva a nivel de bits disyunción exclusiva a nivel de bits desplaza un numero n de bits a la izquierda desplaza un numero n de bits a la derecha Tabla 4.10 - Operadores de Bits
A continuación se presentan algunos ejemplos: program operadoresDeBits_1; var a, b: Byte; // Enteros de 1 byte sin signo c, d: ShortInt; // Enteros de 1 byte con signo begin a := 89; { Se le asigna a la variable 'a' el valor de 0101 1001 } 54
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
b := not a; { Se niega cada uno de los bits de 'a', entonces a la variable 'b' se le asigna el valor de 1010 0110 que representa el valor 166 } c := 89; { se le asigna a la variable 'c' el valor de 0101 1001 } d := not c; { Se niega cada uno de los bits de 'c', entonces a la variable 'd' se le asigna el valor de 10100110 que representa el valor -90 } end. program operadoresDeBits_2; var a, b, c: Byte; // Enteros de 1 byte sin signo begin a := 89; { Se le asigna a la variable 'a' el valor de 0101 1001 } b := 113; { Se le asigna a la variable 'b' el valor de 0111 0001 } c := a and b; { Se opera cada par de bits de las variables tomando los ceros como valor falso y unos como valor verdadero y se aplica a cada par la operación ‘and’ lógica. Por lo tanto la variable 'c' recibe el valor de 0101 0001 que representa el valor de 81 } c := a or b; { Al igual que en el caso anterior se opera cada par de bits, y se aplica a cada par la operación ‘or’ lógica.
Por lo tanto la variable 'c' recibe el valor de 0111 1001 que representa el valor de 121 } c := a xor b; { Al igual que en el caso anterior se opera cada par de bits, y se aplica a cada par la operación ‘xor’ lógica.
end.
Por lo tanto la variable 'c' recibe el valor de 0010 1000 que representa el valor de 40 }
program operadoresDeBits_3; var a, b: Byte; // Enteros de 1 byte sin signo m, n: ShortInt; // Enteros de 1 byte con signo x, y: Integer; // Enteros de 4 bytes con signo begin a := 113; { Se le asigna a la variable 'a' el valor de 0111 0001 } b := a shl 3; { Se desplaza 3 bits del numero hacia la izquierda, como la variable sólo puede contener 8 bits, los tres bits más significativos se perderán y los tres bits menos significativos se rellenarán con ceros. Por lo tanto el valor que recibe la variable 'b' será 1000 1000, que representa el valor 136} m := 113; { Se le asigna a la variable 'a' el valor de 0111 0001 } 55
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
n := m shl 3; { La operación es igual a la anterior, la diferencia está en la representación de ese conjunto de bits, esto es, el valor que recibe la variable 'n' será 1000 1000 (igual al anterior), pero en este caso representa el valor -120} x := 113; { Se le asigna a la variable 'a' el valor de 0000 0000 0000 0000 0000 0000 0111 0001 } y := x shl 3; { La operación es similar a la anterior, la diferencia está en que al tener más espacio, los bits más significativo no se pierde, por lo tanto el valor que recibe la variable 'y' será 0000 0000 0000 0000 0000 0011 1000 1000. En este caso representa el valor 904 } a := 113; { Se le asigna a la variable 'a' el valor de 0111 0001 } b := a shr 3; { Se desplaza 3 bits del numero hacia la derecha, los tres bits menos significativos se perderán y los tres bits más significativos se rellenarán con ceros. Por lo tanto el valor que recibe la variable 'b' será 0000 1110, que representa el valor 14. Para los otros tipos de datos el resultado será el mismo.} end. 6° Operador de Pertenencia: Sirven para poder determinar si un valor pertenece o no a un conjunto dado. En Pascal, un conjunto se representa por un grupo de valores ordinales del mismo tipo encerrado entre corchetes y separados por comas. Por ejemplo, [3, 5, 7, 9] es un conjunto de enteros , [a’, ‘e’, ‘i’, ‘o’, ‘u’] es un conjunto de caracteres, [‘A’..’Z’] conjunto formado por la le tras mayúsculas 10, letras. La tabla 4.11 muestra este operador: Operador in
Operación pertenece a Tabla 4.11 - Operador de conjunto
El valor devuelto por este operador será de tipo Boolean, esto es true o false. A continuación se presentan algunos ejemplos: program opradoresDePermanencia; var a , b: Integer; m: Char; r1, r2, r3, r4: Boolean; begin a := 25; b := 33; m := 'h'; 10
La expresión ‘A’..’Z’ es un sub rango de datos que contiene todos los caracteres en la taba ASCII que se encuentren entre la ‘A’ y la ‘Z’. Si vemos la tabla ASCII que aparece en el primer capítulo, encontraremos que estos caracteres coinciden con las letras
mayúsculas del alfabeto inglés.
56
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
r1 := a in [10, 20, 30, 40, 50]; / Falso, 25 no pertenece al conjunto r2 := a in [15..45]; // Verdadero, 25 pertenece al conjunto de valores que se encuentra entre 15 y 45; r3 := not(b in [10, 20, 30, 40, 50]); // Verdadero: 33 no pertenece al conjunto. Observe la sintaxis, estará mal si se hace b not in [10, 20, 30, 40, 50]. r4 := m in ['A'..'Z']; // Falso, 'h' no es una letra mayúscula. end. Reglas de prioridad de operadores: cuando en una expresión se encuentren varios operadores, la forma cómo se evalúen los elementos de la misma se rigen mediante una serie de reglas. El no seguir estas reglas cuando se construye una expresión nos llevará a resultados incorrectos. Estas reglas son las siguientes: 1ra prioridad los paréntesis ( ): si en una expresión, se encuentra una porción de ésta encerrada entre paréntesis, entonces la porción encerrada se evaluará antes que cualquier otra parte de la expresión. Por ejemplo en la instrucción de asignación: a := 4.5 * (2 + 1.65); primero se evalúa la operación 2 + 1.65 y el resultado se multiplica a 4.5, por lo tanto se le asigna a la variable a el valor de 16.425. 2da prioridad la operación unaria - : cuando el signo menos (-) se emplea para cambiar de signo a un operador, esta operación tiene prioridad sobre los otros operadores. Por ejemplo en la instrucción a := b * -c; primero se cambia el signo a la variable c y luego se multiplica a la variable b. 3ra prioridad la multiplicación, división, shl y shr 4ta prioridad la suma y resta: esto quiere decir que por ejemplo en a := b + c * d; primero se evalúa c * d y luego este resultado se suma a la variable b. 5ta prioridad los operadores lógicos 6ma prioridad los operadores de relación: esto hace que una expresión lógica deba ser escrita como sigue: bool1 := (a > b) and (c <>0); si se quitarán los paréntesis habría un error porque por la prioridad se intentaría evaluar primero b and c. 11 Como regla adicional se establece que en una expresión, si se encontraran dos operadores con la misma prioridad, se evaluará primero el que se encuentre más a la izquierda. Esto último es muy importante porque si quisiéramos evaluar la fórmula a , y escribimos la instrucción de asignación x := a/b*c; estaremos cometiendo x = b×c
un error debido a que por la regla anterior primero se evalúa a/b y el resultado se
11
En el lenguaje C y C++ la prioridad la tienen los operadores relacionales por encima de los operadores lógicos, por lo que no se requiere agregar los paréntesis.
57
multiplica por c, con lo que estaríamos evaluando la
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo fórmula x = a × c , diferente b
a la
original. La instrucción escrita correctamente debería ser x:=a/(b*c); ó x:=a/b/c; Las funciones son parte importante en una expresión, estos elementos nos permiten simplificar el contenido de una expresión, esto debido a que los cálculos que están inmersos en una función están escritos en otra parte; en la expresión sólo se coloca el nombre de la función seguida de los parámetros que requiere. Las funciones en un lenguaje de programación se incorporan a través de bibliotecas o también pueden ser parte del núcleo del mismo. Para evitar tener que perder el tiempo escribiendo código innecesario, el programador debe conocer muy bien las funciones que existen en el lenguaje de programación, esto le ayudará a enfocar sus esfuerzos en las partes más importantes del programa que está elaborando. A continuación se estudiará a fondo las funciones que vienen incorporadas en el núcleo de Pascal, denominadas funciones estándar, y que son similares a la mayoría de lenguajes de programación, de modo que el lector pueda darse una idea del potencial de las mismas. Funciones aritméticas: Función abs(n) sqr(n) sqrt(n)
Argumento entero o real entero o real entero o real
Tipo de resultado igual al del argumento igual al del argumento siempre real
Resultado Valor absoluto: | n | Eleva al cuadrado el argumento: n2 Obtiene .la raíz cuadrada de n
Ejemplos: program usoDeFuncionesAritmeticas; var a, b, c: Integer; r, s, t: Real; begin a := 7; b := -25; r := -3.56; s := 36.21; c := abs(b); {la variable c recibe el valor de 25} t : = abs(r); {la variable t recibe el valor de 3.65} c := sqr(a); {la variable c recibe el valor de 49} t := sqr(s); {la variable t recibe el valor de 1311.1641} t := sqrt(a); {la variable t recibe el valor de 2.6457513} t := sqrt(s); {la variable t recibe el valor de 6.0174746} c := sqrt(a); {error: el resultado es un número real, no se puede asignar a c} end. Funciones trigonométricas: Función pi sin(n) cos(n) arcTan(n)
Argumento entero o real en radianes entero o real en radianes entero o real en radianes
Tipo de resultado siempre real siempre real siempre real siempre real
Resultados El valor de π: 3.141592
El seno del ángulo n El coseno del ángulo n El arco cuya tangente es n.
Ejemplos: 58
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
program usoDeFuncionesTrigonometricas; var a, b : Real; begin a := 2.54; {la variable a representa 2.45 radianes} b := sin(a); {la variable b recibe el valor de 0.565956} b := cos(a); {la variable b recibe el valor de -0.824435} a := 0.3456; b := arctan(a); {a variable b recibe el valor de 0.332750 radianes} a := 28; {la variable a representa 28 grados sexagesimales} b := sin(a * pi / 180); {la variable b recibe el valor de 0.4694720 } b := cos(a * pi / 180); { la variable b recibe el valor de 0.882948 } a := 0.3456; b := arctan(a) * 180 / pi; { la variable b recibe el valor de 19.065150°} end. Ejercicio: Si β es igual al arco cuyo seno es de 0.7542, ¿cuánto vale β? Solución: Dado que el Pascal no posee la función arco seno, debemos buscar la forma de hallar el valor de β por otros medios. Por las reglas de la trigonometría se sabe que el seno de β es 0.7542, por lo tanto en un triángulo rectángulo se
tendrá el siguiente esquema:
1
0.7542
β
El cateto que falta se puede evaluar como: 12 - 0.75422 Con lo que se tienen:
1
0.7542
β
12 - 0.75422 Por lo tanto podemos decir que β es el arco tangente de 0.7542 ÷
12 - 0.75422
Pascal será: b := arcTan(0.7542/sqrt(1-sqr(0.7542)))*180/pi; {la variable b recibe el valor de 48.955517° grados sexagesimales} Observar que se emplea b y no β en la instrucción. La instrucción que permite calcular β en
59
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Funciones exponenciales: Función exp(n) ln(n)
Argumento Tipo de resultado Resultado Entero o real siempre real Calcula el valor de en. Entero o real siempre real Calcula el logaritmo natural de n.
Ejemplos: program usoDeFuncionesExponenciales; var a, b : Real; begin a := 2.54; b := exp(a); {la variable b recibe el valor de 12.679671 } b := ln(a); {la variable b recibe el valor de 0.932164} b := exp(1); {la variable b recibe el valor de 2.718282 valor de e} end. Ejercicio: Calcular el valor de 12.452.5 y el de log 10(2.54) Solución: El lenguaje Pascal no tiene funciones ni operadores que permitan calcular el valor de un número elevado a una potencia dada (x y) o el logaritmo decimal, como lo tiene el lenguaje C (pow(x, y) y log10(x) de la biblioteca math.h) o el lenguaje Basic (el operador ^ para calcular x y), por eso debemos solucionarlo aplicando las propiedades de los logaritmos, entonces: Dado que queremos calcular: Z = X Y Al aplicar el logaritmo natural a ambos factores se tiene: ln(Z) = ln(X Y) Aplicando las propiedades de los logaritmos se tiene: ln(Z) = Y·ln(X) Al aplicar una operación inversa ambos factores se tiene: eln(Z) = e Y·ln(X) Se cancelan las operaciones en el primer factor y se tiene: Z = e Y·ln(X) Por otro lado queremos calcular: Y = log10(X) log ( p ) Se sabe que log n ( p ) = K , por lo tanto Y = ln(X) ÷ ln (10) log K (n ) Las instrucciones que permiten calcular estas operaciones en Pascal serán: program usoDeFuncionesExponenciales; var a, b : Real; begin a := exp(2.5 * ln(12.45)); {la variable a recibe el valor de 546.919463} b := ln(2.54) / ln(10); {la variable b recibe el valor de 0.404834} end. 60
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Funciones de cambio de tipo: Función
Argumento
trunc(n) int(n) round(n) frac(n)
entero o real entero o real entero o real entero o real
Tipo de resultado entero real entero real
Resultado Devuelve la parte entera de n. Devuelve la parte entera de n pero en formato real. Redondea el valor de n al 0.5. Devuelve la parte fraccionaria de n.
Ejemplos: program usoDeFuncionesDeCambioDeTipo; var r1, r2 : Real; i1, i2 : Integer; begin r1 := 23.756; i1 := trunc(r1); {la variable i1 recibe el valor de 23 } i2 := int(r1); {ERROR: el resultado es un valor real} r2 := int(r1); {la variable r2 recibe el valor de 23.0 } i1 := round(r1) {la variable i1 recibe el valor de 24 } r2 := frac(r1) {la variable r2 recibe el valor de 0.756 } end. Ejercicio: Se desea redondear el valor de 23.736 al segundo decimal. program redondeoDecimal; var r1, r2: Real; i1: Integer; begin r1 := 23.736; r2 := r1 * 100; {la variable r2 recibe el valor de 2373.6} i1 := round(r2); {la variable i1 recibe el valor de 2374} r1 := i1 / 100; {la variable r1 recibe el valor de 23.73} end. Funciones ordinales: Función Argumento ord(n) valor ordinal chr(n) entero, entre 0 y 255 pred(n) valor ordinal succ(n) valor ordinal
Tipo de resultado entero caracter
Resultado La posición de n en la tabla a la que pertenece Devuelve el caracter ASCII correspondiente al valor de n.
igual al del argumento igual al del argumento
Da el valor que precede a n en la tabla a la que pertenece Da el valor que sucede a n en la tabla a la que pertenece
Ejemplos: program usoDeFuncionesOdinales; var i1, i2, i3 : Integer; c1, c2, c3 : Char; b1, b2, b3 : Boolean; 61
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
begin c1 := ‘A’; b1 := true; b2 := false; i1 := 75 i1 := 56; i2 := pred(i1); i3 := succ(i1); c1 := ‘F’; c2 := pred(c1); c3 := succ(c1); b1 := true; b2 := pred(b1); b3 := succ(b2); end.
i1 := ord(c1); i1 := ord(b1); i1 := ord(b2); c1 := chr(i1);
{la {la {la {la
variable variable variable variable
i1 recibe el valor de 65} i1 recibe el valor de 1} i1 recibe el valor de 0} c recibe el valor de ‘K’}
{la variable i2 recibe el valor de 55} {la variable i3 recibe el valor de 57} {la variable c2 recibe el valor de ‘E }’ {la variable c3 recibe el valor de ‘G’} {la variable b2 recibe el valor de false} {la variable b3 recibe el valor de true}
Ejercicio: Se desea asignar a una variable el mayor valor entre otras dos, por ejemplo si la variable a tiene el valor de 3 y la variable b tiene 5, la variable c debe recibir el valor de 5. La operación debe funcionar correctamente sin saber cuáles serán los valores que se asignarán a las variables a y b. Solución: La expresión que diseñemos debe asignar el mayor valor entre dos variables. Como no sabemos de antemano el valor de las variables, la expresión debe de alguna manera anular el menor valor y hacer prevalecer el mayor. La solución tiene que ir por el siguiente camino: c := a * exp1 + b * exp2; Donde debemos encontrar una expresión exp1 que anule el valor de la variable a si este es menor que el de la variable b y lo mantenga en caso contrario. Por otro lado la expresión exp2 debe hacer algo similar, esto es anular el valor de la variable b si es menor que el de la variable a o mantenerlo en caso contrario. La manera de anular un valor se puede hacer multiplicando el valor por cero (0) y de mantenerlo multiplicándolo por uno (1). Por lo tanto las expresiones exp1y exp2 deben dar como resultado 0 ó 1. La forma más sencilla de obtener 0 ó 1 en una expresión es aplicando la función ord a una expresión lógica (ord(false) da como resultado 0 y ord(true) da como resultado 1). Por lo tanto la solución para el ejercicio será: c := a * ord(a>b) + b * ord(b>a); Observe que si a es mayor que b, la expresión a>b da verdadero y la expresión b>a da falso, por lo tanto se anula el valor de la variable b, y se anulará el valor de la variable a si el valor de b es mayor que el de a. 62
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Ejercicio: Dada una variable de tipo char que contiene un caracter cualquiera, convertir ese caracter en una letra minúscula si el caracter es una letra mayúscula, pero mantener su valor inalterado si es otro caracter. Solución: En este caso hay que observar la ubicación de las letras mayúsculas y minúsculas en la tabla ASCII, ahí se puede observar que las letras mayúsculas se encuentran agrupadas consecutivamente desde la ‘A’ hasta la ‘Z’ en una zona de la
tabla (como veremos más adelante, no será necesario conocer la posición exacta de los caracteres en la tabla, ni su código ASCII), y que las letras minúsculas se encuentran también agrupadas consecutivamente en otra parte de la tabla en una ubicación posterior a la de las letras mayúsculas (esto último si es importante). Al estar agrupadas las letras, se tendrá que si tomamos una letra minúscula cualquiera y su correspondiente letra mayúscula y restamos sus códigos ASCII siempre obtendremos el mismo valor. Esto es fácil de demostrar: Código ASCII de la ‘a’ = T
Código ASCII de la ‘A’ = P
…
A
B
C
D
P
P+1
P+2
P+3
…
X
Y
Z
P+23
P+24
P+26
…
a
b
c
d
T
T+1
T+2
T+3
…
X
Y
z
T+23
T+24
T+26
…
Según esto: El código ASCII de ‘a’ menos El código ASCII de ‘d’ menos El código ASCII de ‘y’ menos
el código ASCII de ‘A’ = T – P el código ASCII de ‘D’ = (T + 3) – (P + 3) = T - P el código ASCII de ‘Y ’ = (T + 24) – (P + 24) = T - P Luego, si tenemos una letra mayúscula será suficiente sumarle a su código ASCII, la diferencia entre los códigos ASCII de la ‘a’ y la ‘A’ y obtendremos su
correspondiente letra minúscula. Por otro lado, la respuesta a este problema tiene que ir por un camino similar al del problema anterior, esto debido a que hay una condición que debemos evaluar, esto es que si el caracter no es una letra minúscula debemos mantenerlo inalterado, y sólo se debe cambiar las mayúsculas. Finalmente, como se trata de caracteres se tendrá que hacer una conversión del caracter a un entero y luego de aplicarle la expresión correspondiente volverlo a convertir a caracter. La expresión que se obtiene es la siguiente: car := chr( ord(car) + ( ord(‘a’) – ord(‘A’) ) * ord( (car >= ’A’) and (car <= ‘Z’) ) ); En la expresión se puede apreciar que si el caracter que contiene la variable car no es una letra mayúscula se anula la suma y el caracter no se altera, pero si es una letra mayúscula se le aplica la suma y se convierte en una minúscula. 63
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Funciones de Valor Lógico Función odd(n) eof(n) eoln(n)
Argumento entero variable de archivo variable de archivo
Tipo de res. Lógico Lógico Lógico
Resultado Nos indica si el argumento es impar o no. Nos indica si llegamos al final de un archivo 12. Nos indica si llegamos al final de una línea en un archivo.
Ejemplos: program usoDeFuncionesDeValorLogico; var i1, i2 : Integer; b1, b2: Boolean; begin i1 := 56; i2 := 15; b1 := odd(i1); {la variable b1 recibe el valor de false } b2 := odd(i2); {la variable b2 recibe el valor de true } end. Funciones aleatorias: Función random random(n)
Argumento Tipo de resultado real en el rango: [0,1[ entero entero en el rango: [0,n-1]
Ejemplos: program usoDeFuncionesAleatorias1; var i: Integer; r: Real; begin r := random; {la variable r recibe un valor aleatorio p. e.: 0.5435682} i := random(10); {la variable i recibe un valor aleatorio entre 0 y 9, p. e.: 7} end. Ejercicio: Determinar un valor aleatorio entre 12 y 28 Solución: Como los valores aleatorios no se pueden definir en un rango dado, tendremos que determinar un valor aleatorio en el rango [0, n-1], donde el tamaño del rango sea igual al del rango [12, 28], de este modo sólo se tendrá que agregar al valor obtenido por la función random el límite inferior del rango para obtener el valor deseado. Así: program usoDeFuncionesAleatorias2; var limInf, limSup, valorAleat, tamRango: Integer; begin limInf : = 12; limSup := 28; tamRango := limSup – limInf + 1; {la variable tamRango recibe el valor de 17} valorAleat := random(tamRango); {la variable valorAleat recibe el valor ente 0 y 16} valorAleat := valorAleat + limInf; {la variable valorAleat recibe el valor ente 12 y 28} end. 12
El manejo de archivos se verá más adelante
64
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Ejercicio: Determinar un número aleatorio entero, múltiplo de 3, que se encuentre entre un límite inferior y uno superior. Si los límites no han sido dados correctamente, esto es que límite inferior sea mayor que el superior la respuesta debe ser cero. Solución: La última condición del problema es muy fácil de solucionar, ya que la respuesta que obtendremos se anulará si no cumple las condiciones, esto ya se ha hecho en ejercicios anteriores. En el ejercicio anterior se solucionó el problema de determinar un número aleatorio dentro de un rango dado, el problema está en que el número aleatorio debe ser múltiplo de 3. La manera de conseguirlo es determinar otro rango, a partir de los límites, que permita determinar un número aleatorio el cual al multiplicase por 3 de un valor dentro de los límites iniciales. Por ejemplo si los límites fueran [7, 20], podemos determinar un rango como [3,6], cualquier valor que tomemos de este último rango al ser multiplicado por 3 estará entre 7 y 20 y será múltiplo de 3. En estos términos se presenta la siguiente solución: program usoDeFuncionesAleatorias3; var limInf, limSup, nuevoLimInf, nuevoLimSup, valorAleat, tamRango: Integer; respuestaValida: Boolean; begin randomize; limInf := ; limSup := ; { indica un valor dado inicialmente} nuevoLimInf := limInf * ord(limInf mod 3 = 0) + (limInf + 1) * ord( (limInf + 1) mod 3 = 0) + (limInf + 2) * ord( (limInf + 2) mod 3 = 0); {se calcula el menor valor dentro del rango que es múltiplo de 3} nuevoLimInf := nuevoLimInf div 3; {se calcula el límite inferior del nuevo rango} nuevoLimSup := limSup * ord(limSup mod 3 = 0) + (limSup - 1) * ord( (limSup - 1) mod 3 = 0) + (limSup - 2) * ord( (limSup - 2) mod 3 = 0); {se calcula el mayor valor dentro del rango que es múltiplo de 3} nuevoLimSup := nuevoLimSup div 3; {se calcula el límite superior del nuevo rango} tamRango := nuevoLimSup – nuevoLimInf + 1; valorAleat := random(tamRango) + nuevoLimInf; {se obtiene un valor aleatorio en el segundo rango} valorAleat := valorAleat * 3; {se obtiene un valor aleatorio, múltiplo de 3, en el rango original} respuestaValida := limSup > limInf; valorAleat := valorAleat * ord(respuestaValida); {se anula o no el resultado} end.
65
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Procedimientos Aritméticos Estándar Aunque estos elementos no se pueden colocar dentro de una expresión ya que son procedimientos y no funciones, y por lo tanto no devuelven resultados, es bueno conocerlos porque son muy útiles. Procedimiento ramdomize dec(x) dec(x, n) inc(x) inc(x, n)
Argumento Valor ordinal x debe ser ordinal, n entero Valor ordinal x debe ser ordinal, n entero
Descripción Inicializa la semilla para la función random modifica x con su predecesor modifica x con su n-ésimo predecesor modifica x con su sucesor modifica x con su n-ésimo sucesor
Ejemplos: program usoDeProcedimientosAritmeticos; var i: Integer; car: Char; begin i := 34; inc(i); {la variable i se modifica con el valor de 35} i := 34; dec(i); {la variable i se modifica con el valor de 33} i := 34; inc(i,5); {la variable i se modifica con el valor de 39} i := 34; dec(i,7); {la variable i se modifica con el valor de 27} {la variable car se modifica con el valor de ‘L’} car := ‘K’; inc(car); car := ‘K’; dec(car); {la variable car se modifica con el valor de ‘J’} car := ‘K’; inc(car,5); {la variable i se modifica con el valor de ‘P’} {la variable i se modifica con el valor de ‘D’} car := ‘K’; dec(car,7); end.
Instrucciones que permiten la salida y el ingreso de datos Cuando se escribe un programa se debe pensar que lo hacemos para obtener resultados. Los ejemplos y ejercicios que hemos mostrado hasta ahora están incompletos, esto debido a que si los escribimos en un entorno de desarrollo y los compilamos, si es cierto que es seguro que no encontremos errores, si los ejecutamos no veremos los resultados. La razón para este comportamiento es que no hemos colocado en los programa instrucciones que nos permitan ver el contenido de las variables que tienen los resultados esperados. Por otro lado, si los programas que presentamos los ejecutamos una y otra vez vamos a obtener los mismos resultados (salvo el caso de las funciones aleatorias). La razón es que los valores que se asignan a las variables se dan en tiempo de compilación y no en tiempo de ejecución. Esto quiere decir que el programa ya compilado tiene la orden de asignar un valor predeterminado a una variable, y este valor no cambiará. Por eso se requieren órdenes que permitan dar valores a las variables cuando el programa se esté ejecutando, estos valores se deben dar por un medio externo al programa. Lo que se va a estudiar en esta sección son instrucciones que permiten el ingreso y salida de datos, básicamente se estudiarán las instrucciones de salida y entrada de datos del Pascal que son los procedimientos writeln, write, read y readln. 66
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Procedimiento writeln Este procedimiento permite mostrar el contenido de las variables y de expresiones en forma de texto en el medio estándar de salida (la consola o el monitor del computador). Luego que termina de realizar la tarea imprimir todas las variables y expresiones que se le han entregado por medio de parámetros, la instrucción efectúa un cambio de línea de modo que la siguiente orden de entrada o salida que se ejecute en el programa se realicen un línea por debajo de los que acaban de aparecer. La sintaxis para esta orden se presenta a continuación: writeln
(
expresión
)
;
tamaño de campo ,
Ejemplos: 1° Empleando número enteros: program usoDelProcedimientoWritelnConNumerosEnteros; var a, b, c: Integer; begin En este punto la pantalla se verá así: a:= 2; b:= -34; c:=123; 2 {Imprimiendo un valor a la vez} -34 writeln(a); Cursor 123 writeln(b); _ writeln(c); {Imprimiendo valores con etiquetas uno a la vez} writeln(‘A= ‘,a); writeln(‘B= ‘,b); writeln(‘C= ‘,c); {Imprimiendo valores en la misma orden} writeln(a, b, c); {Igual pero con etiquetas} writeln(‘A= ‘,a, ‘B= ‘,b, ‘C= ‘,c); espacios
{Colocando más espacios en las writeln(‘A= ‘,a, ‘ B= ‘,b, ‘ C= ‘,c); espacios
A= 2 B= -34 C =123 _
Cursor
Los valore se pegan unos con otros 2-34123 _
A= 2B= -34C= 123 _
A= 2 B= -34 C= 123 _
67
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
{Colocando un tamaño de campo. Mediante un valor entero se define el número mínimo da caracteres que se emplearán para imprimir un valor. Si el valor tiene menos caracteres que del tamaño dado, se completa con espacios en blanco, si tiene más, se ignora el tamaño.} 2 -34 123 writeln(a:7, b:7, c:7); _ 4 espacios + 3 dígitos 6 espacios +1 dígitos
writeln(a:2, b:2; c:2); 2-34123 _
{Imprimiendo valores tabulados} writeln(a,b,c); writeln(b,c,a); writeln(c,a,b); writeln(a,' ',b,' ',c); writeln(b,' ',c,' ',a); writeln(c,' ',a,' ',b); writeln(a:9,b:9,c:9); writeln(b:9,c:9,a:9); writeln(c:9,a:9,b:9); end. 2° Empleando número reales: program usoDelProcedimientoWritelnConNumerosReales; var r, s: Real; begin r:= 34.67; s:=-123.0236587; writeln(r); writeln(s); writeln(‘R= ‘,r); writeln(‘S= ‘,s); writeln(r:10:3); writeln(s:10:3); writeln(s:10:4);
3 dígitos 1 espacios +1 dígitos
2-34123 -341232 1232-34 _ 2 -34 123 -34 123 2 123 2 -34 _ 2 -34 123
-34 123 2
123 2 -34
_
3.4670000000E+01 -1.2302365870E+02 _
R= 3.4670000000E+01 S= -1.2302365870E+02 _ 34.670 -123.024 -123.0237 _
Redondeo
end. 68
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Procedimiento write Es un procedimiento similar a writeln pero con la diferencia que luego de realizar su tarea no efectúa un cambio de línea. Ejemplos: program usoDelProcedimientoWrite; var a, b, c: Integer; En este punto la pantalla se verá así: begin 2-34123_ a := 2; b:= -34; c:=123; write(a); Cursor write(b); write(c); writeln; {pone el cursor en la siguiente línea} {Cambio de línea: el efecto del cambio de línea se produce cuando se imprimen los caracteres cuyo código ASCII en 10 (line feed) y 13 (carriage return)} write(c, #10,a);
123 2_
{Al imprimir el caracter #10 el cursor baja} writeln; write(c, #13,a);
Cursor
223 Cursor
{Al imprimir el caracter #13 el cursor retrocede al inicio de la línea} writeln; write(c, #13, #10,a); {La combinación del caracter #13 y el #10 producen el efecto del cambio de línea completo} writeln;
123 2_ Cursor
Por lo tanto hacer writeln(a) es equivalente a hacer write(a, #13, #10); Procedimiento read Este procedimiento permite ingresar datos al programa, esto es asignar a las variables del programa valores que se ingresan desde la unidad estándar de entrada del sistema que en la mayoría de casos corresponde a la consola. Cuando se ejecuta este procedimiento por primera vez, el programa se detiene para permitir al usuario escribir en la consola un flujo de texto con los datos que desea ingresar. Luego de escribirlos el usuario presiona la tecla ENTER y en ese momento los datos pasan a una zona de memoria temporal denominada “buffer de entrada”, es allí 69
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
donde el procedimiento read comienza a explorar uno a uno cada caracter transformándolo en una representación binaria y almacenándolo en las variables del programa de acuerdo al tipo de éstas. Luego de realizar la operación, la información que no se utilizó queda almacenada en al “buffer” para ser leídas en otras operaciones de lectura. La siguiente vez que se ejecute la orden read, primero verá si hay algo en el buffer, si lo hay toma los datos que requiere de allí, si no detiene el programa y el proceso anterior se repite. Si la cantidad de datos ingresados es menor al que contiene la orden read, el proceso se detiene luego de asignar el último dato para solicitar el resto. read
(
Ejemplos:
variable
)
;
,
program usoDelProcedimientoRead; var i1, i2: Integer; r1: Real; begin read(i1, r1, i2);
Aquí el programa se detiene, el usuario debe ingresa el siguiente texto: 57 23.45 145 ↵ Enter {El ‘57’ se convierte en una representación binaria y se asigna a la variable i1, lo mismo pasa con el espacios ’23.45’ y ‘145’ que se asignan a r1 e i2 respectivamente } Como el ‘buffer’ está vacío el programa se detiene, el usuario ingresa el siguiente texto: read(i1); 391 0.8367 5 ↵ Enter {El ‘391’ se convierte en una representación
binaria y se asigna a la variable i1, el resto del texto
se queda en el ‘buffer’}
read(r1); read(i2); {El ‘programa no se detuvo, asignó el ‘0.8367’ y el ’5’ a las variables r1 e i2 respectivamente’}
end.
Procedimiento readln Es similar al procedimiento read, pero a diferencia de éste último, luego de realizar la operación, la información que no se utilizó es eliminada del “buffer”. Ejemplos: 70
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
program usoDelProcedimientoReadln; var i1, i2: Integer; r1: Real; begin readln(i1);
Aquí el programa se detiene, el usuario debe ingresa el siguiente texto: 57 23.45 145 ↵ Enter {El ‘57’ se convierte en una representación binaria
y se asigna a la variable i1, el resto de información
se borra del ‘buffer’}
readln(r1) {Aquí se vuelve a detener el programa para esperar un dato para r1, cuando lo obtiene borra el resto del ‘buffer’ } readln(i2); {Aquí se vuelve a detener el programa para esperar un dato para i2, cuando lo obtiene borra el resto del ‘buffer’ } end. Lectura de Caracteres y Cadenas de Caracteres La lectura de caracteres y cadenas de caracteres son casos especiales en el proceso de lectura, en primer lugar, en estos casos no se transforman los datos del buffer como se hacen con los valores numéricos. Por otro lado su comportamiento es peculiar, los ejemplos siguientes ilustran estos detalles. Ejemplos: 1° Lectura de caracteres: program lecturaDeCaracteresYCadenas; var c1, c2, c3: Char; s1: String; i1, i2: Integer; begin {Lectura de caracteres} Si se ingresan los caracteres como sigue: readln(c1, c2, c3); A B C↵
{Lo que se asigna es el caracter ‘A’ a la variable
Enter
c1, el ‘ ‘ (espacio) a la variable c2 y la ‘B’ a c3 espacios el resto se borra’} Si se ingresan los caracteres como sigue:
readln(c1, c2, c3);
ABC ↵
{Lo que se asigna es el caracter ‘A’ a la variable c1, el ‘B‘ a la variable c2 y el ‘C’ a c3’} readln(c1, i1);
Enter
La lectura es correcta si primero se pretende leer el caracter y luego el número: A 123 ↵
{Se asigna es el caracter ‘A’ a la variable c1 y
Enter
el 123 a i1’}
71
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
La lectura no es correcta si primero se pretende leer el número y luego el caracter: 123 A ↵ {Se asigna el 123 a i1’ y el caracter ‘ ’ (espacio) Enter
readln(i1, c1);
a la variable c1}
readln(c1, i1);
123A ↵
{Se produce un error porque 123A no es un
Enter
formato válido para un entero. Se interrumpe el programa’}
{Lectura de cadenas de caraacteres} readln(s1);
Juan López Pérez ↵ Enter
{Lectura es correcta se asigna ‘Juan López Pérez’ a la
readln(s1, i1, i2); {Lectura NO es correcta se asigna
variable s1}
Juan López Pérez 123 47 ↵ Enter
‘Juan López Pérez 123 47’ a la variable s1 y se
detiene el programa a la espera de ingresar valores para i1 e i2} readln(i1, i2, S1);
123 47 Juan López Pérez↵ Enter
{Lectura NO es correcta se asigna 123 a la variable i1 y 47 a la variable i2, pero a la variable se
espacio
le asigna ‘ Juan López Pérez’ con un espacio al inicio y eso está mal }
{La manera correcta de leer cadenas de caracteres es:} readln(S1); readln(i1, i2); {Cada variable de tipo String debe ir en una orden readln por separado} end.
Ejemplos de programas secuenciales: A continuación presentamos algunos ejemplos de problemas que se solucionan con programas que emplean instrucciones de entrada y salida así como expresiones. 1. Escriba un programa en Pascal que permita leer las coordenadas de dos puntos en un plano y que calcule la distancia entre ellos y el ángulo que forma el segmento de recta con la horizontal. Datos: Se parte de las coordenadas de dos puntos P1, y P2. Por lo tanto se tiene como datos, que serán leídos, los valores de X1 e Y1 del punto P1 y X2 e Y2 del punto P2. 72
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Resultados: Lo que se espera del programa es que nos determine la distancia (D) entre los puntos P1 y P2, y por otro lado el ángulo ( β) que hace el segmento de recta que une P1 y P2 con la horizontal. P2=(X2, Y3) Cálculos: D 2 2 D = ( X 2 − X 1) + (Y 2 − Y 1) … f1 Y2 - Y1 P1=(X1, Y1)
Y 2 − Y 1 … f2 X 2 − X 1
β = Tan −1
ß X2 - X1
Diagrama de Flujo: Un diagrama de flujo puede ayudar a visualizar la solución al problema. Inicio P1, P2 D = f 1 ß = f 2 D, ß Fin
Programa: Program calculaLaDistanciaYAnguloEntreDosPuntos; var x1, x2, y1, y2, d, ang: Real; begin {Lectura de datos:} write(‘Ingrese las coordenadas de P1: ‘); readln(x1, y1); write(‘Ingrese las coordenadas de P2: ‘); readln(x2, y2); {Calculamos resultados:} d := sqrt( sqr(x2-x1) + sqr(y2-y1) ); ang := arcTan( (y2-y1) / (x2-x1) ) * 180 / pi; {Mostramos resultados:} writeln(‘La distancia entre P1 y P2 es: ‘, D:7:3); writeln(‘EL ángulo que forma el segmento con la horizontal es: ‘, ang:7:2, ‘°’); end. 73
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
2. Escriba un programa que permita leer los datos que se requieran para calcular e imprimir el valor de −
( )×
α =
2.5
cos 1 φ φ
5
( ) + Ω senh δ δ
log 5
5
(
tan δ
4 + 3 φ
)
cos -1 = arco coseno log5 = logaritmo en base 5 donde : tan = tangente x −x senh(x) = e − e 2
Datos: El valor de se calcula a partir de , y , por lo que estos últimos serán los datos del programa. Como el lenguaje no permite el uso de estos símbolos se les tendrá que dar nombres alternativos como alfa, phi, delta y omega. Resultados: Lo que se espera del programa es que nos determine el valor de . Cálculos: La solución se debe dar en partes, esto quiere decir que no vamos a calcular todo en una sala expresión, si no que partiremos la expresión de modo que el programa sea más claro y ordenado. Los cálculos podrían hacerse en el siguiente orden: 2 .5
(1)… φ
(2)… φ ( )
(3)… cos −1 ((2))
(4)…
(5)… senh ((4) )
(6)…
(10)… δ 4+(9)
(11)… tan((10) )
δ + Ω
(9)… 3 φ (12)…
(3) × (8) (11)
1
(13)…
5
δ
(5)
(7)… log 5 [(6)]
(8)…
5
(7 )
(12)
Programa: program calculoDeFormulaCompleja; var alfa, phi, delta, omega: Real; {variables principales} { 1} phi25, {2} phiphi25, {3} cos_1, { 4} deltaMasOmega, {5} senh, {6} divRDelta5, {7} log5, {8} raiz5, { 9} raiz3Phi, {10} delta4Mas9, {11} tan10, {12} div3x8_11: Real; begin {Lectura de datos:} write('Ingrese los valores de phi, delta y omega: '); readln(phi, delta, omega); {Cálculos parciales:} {1} phi25 := exp(2.5*ln(phi)); {2} phiphi25 := exp(phi25*ln(phi)); {3} cos_1 := arcTan ( sqrt(1-sqr(phiphi25))/phiphi25); 74
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
{4} deltaMasOmega := delta + omega; {5} senh := (exp(deltaMasOmega)-exp(-deltaMasOmega))/2; {6} divRDelta5 := sqrt(delta)/senh; {7} log5 := ln(divRDelta5)/ln(5); {8} raiz5 := exp( (1/5)*ln(log5)); {9} raiz3Phi := exp( (1/3)*ln(phi)); {10} delta4Mas9 := exp((4+raiz3Phi)*ln(delta)); {11} tan10 := sin(delta4Mas9) / cos(delta4Mas9); {12} div3x8_11 := (cos_1*raiz5)/tan10; {Cálculo de alfa:} alfa := exp((1/5)*ln(div3x8_11)); {Mostramos resultados} writeln('El valor de alfa es: ',alfa:10:5); end. Prueba: Al ejecutar este progarma observaremos el siguiente resultado: Ingrese los valores de phi, delta y omega: 0.8374 0.5432 0.13159 El valor de alfa es: 1.29685 3. Escriba un programa que permita ingresar el nombre de un alumno, su código y sus notas de un curso (cuatro prácticas, las notas de los exámenes 1, 2 y del examen especial). El programa deberá calcular la nota que obtendrá el alumno en el curso. La nota se calculará de la siguiente manera: nota =
3 × pp + 3 × ex1 + 4 × ex 2 10
Donde: pp es el promedio de prácticas eliminando la nota más baja. ex1 es la nota del primer examen ex2 es la nota del segundo examen El examen especial reemplaza a uno de los exámenes en el caso que uno de ellos no se hubiera rendido.
Datos: Se tiene como datos el nombre del alumno y el código, sus notas a las que llamaremos pr1, pr2, pr3, pr4, ex1, ex2, exE. Las notas estarán entre 0 y 20, si un examen no se dio, se ingresará en su lugar el valor -1, si no se rindieron los dos exámenes, el examen especial reemplazará al segundo examen. Tareas que se han de realizar en el programa: - Determinar la nota más baja de prácticas. - Calcula pp: Sumar las cuatro notas de práctica y restarle la menor, el resultado dividirlo entre tres. - Verificar si se rindió el examen 2, si no es así reemplazarlo por el ex. espacial. - Verificar si se rindió el examen 1, si no es así reemplazarlo por el ex. espacial si es que se rindió el examen 2. - Calcular nota del alumno. Resultado: Nota del alumno 75
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Programa: program promedioDeNotas; var pr1, pr2, pr3, pr4, ex1, ex2, exE, prMin, nota: Integer; promPr : Real; nombre, codigo: String; begin {Lectura de datos:} write('Ingrese el código del alumno: '); readln(codigo); write('Ingrese el nombre del alumno: '); readln(nombre); write('Ingrese las notas de práctica (4): '); readln(pr1, pr2, pr3, pr4); write('Ingrese las notas de los exámenes 1, 2 y especial: '); readln(ex1, ex2, exE); {Cálculos:} {1) Tomamos la menor entre pr1 y pr2} prMin := ord(pr1<=pr2)*pr1 + ord(pr2-1)*ex1 + ord((ex1=-1) and (ex2<>-1))*exE; {Decidimos reemplazar o no el examen especial con el examen 2} ex2 := ord(ex2<>-1)*ex2 + ord(ex2=-1)*exE; nota := round((promPr*3 + ex1*3 + ex2*4) / 10); {Mostrar resultados:} writeln('EL alumno: ', codigo, ' ', nombre, ' obtuvo de nota: ', nota); end. 4. Escriba un programa que reciba como dato un valor entero de 4 bytes. El programa deberá manejar el número como si tuviera la forma ABCD, donde A, B, C, D son las representaciones binarias de cada uno de los bytes del número. El programa deberá generar otro número a partir del primero que tengan la forma de C’A’D’B’. Donde C’ contiene lo mismo que C pero con los bits pares invertidos. A’ contiene lo mismo que A
pero los 4 bits más significativos se colocan como los menos significativos y los 4 menos significativos como más significativos. D’ contiene lo mismo que D pero con los bits impares invertidos. B’ contiene lo mismo que B pero con los dos bits menos
significativos intercambiados en posición. Ejemplo:
76
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Datos:
X = 902661613
A 00110101
B 11001101
C
D
10000101
C
11101101
C’
10000101
11010000
A
A’
00110101
01010011
D
D’
11101101
01000111
B
B’
11001101
11001110
Resultado: C’
S = -799848498
11010000
A’ 01010011
D’ 01000111
B’ 11001110
Tareas que se han de realizar en el programa: - Leer el número. - Separar cada byte del número y colocarlas en cuatro variables diferentes. - Tomar la 3ra. variable (C) y proceder a invertir los bits pares. - Tomar la 1ra. variable (A) e intercambiar los cuatro bits más significativos con los cuatro menos significativos - Tomar la 4ta. variable (D) y proceder a invertir los bits impares. - Tomar la 2da. variable (B) e intercambiar los dos bits menos significativos. - Formar nuevamente el número con los cambios hechos, en el orden como se muestran las tareas indicadas arriba. Programa: program EjemploDelManejodeBits; var valor: Integer; // Variable de 4 bytes. a, b, c, d: Integer; a1, b1, c1, d1: Integer; nuevoValor: Integer; mascara, aux: Integer; begin {Leer el número, p. e.: 902661613 ==> 00110101 11001101 10000101 11101101}
write('Ingrese un numero entero grande: '); readln(valor);
{Separar cada byte del número y colocarlas en cuatro variables diferentes: } a := valor; { Recibe el valor 00110101 11001101 10000101 11101101} a := a shr 24; { Al desplazar los bits a la derecha, la variable 'a' toma el valor de 00000000 00000000 00000000 00110101, valor esperado.}
b := valor; b := b shl 8;
{ Recibe el valor 00110101 11001101 10000101 11101101} { Al desplazar los bits a la izquierda, la variable 'b' toma el valor de 11001101 10000101 11101101 00000000} b := b shr 24; { Al desplazar los bits a la derecha, la variable 'a' toma el valor de 00000000 00000000 00000000 00110101, valor esperado.}
77
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
c := valor; { Recibe el valor 00110101 11001101 10000101 11101101} c := c shl 16; { Al desplazar los bits a la izquierda, la variable 'c' toma el valor de 10000101 11101101 00000000 00000000} c := c shr 24; { Al desplazar los bits a la derecha, la variable 'c' toma el valor de 00000000 00000000 00000000 10000101, valor esperado.}
d := valor; { Recibe el valor 00110101 11001101 10000101 11101101} d := d shl 24; { Al desplazar los bits a la izquierda, la variable 'd' toma el valor de 11101101 00000000 00000000 00000000} d := d shr 24; { Al desplazar los bits a la derecha, la variable 'd' toma el valor de 00000000 00000000 00000000 11101101, valor esperado.} { Tomar la 3ra. variable (C) y proceder a invertir los bits pares.} { Para invertir el valor de un bit emplearemos las siguientes reglas: 0 xor 0 = 0 1 xor 0 = 1 ==> Bit xor 0 = Bit 0 xor 1 = 1 1 xor 1 = 0 ==> Bit xor 1 = Bit invertido Entonces debido a que quiere invertir solo los bits pares, formaremos el valor (máscara) 0101 0101 y le aplicaremos la operación xor para conseguir la meta esperada.} { Formamos la máscara}
mascara := mascara := mascara := mascara := mascara := mascara := mascara :=
1; mascara shl 2; mascara + 1; mascara shl 2; mascara + 1; mascara shl 2; mascara + 1;
{ Invertimos los bits pares de C}
c1 := c xor mascara;
// // // // // // //
00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000001 00000100 00000101 00010100 00010101 01010100 01010101, OK
{ 00000000 00000000 00000000 10000101 xor
00000000 00000000 00000000 01010101 = 00000000 00000000 00000000 11010000 { Tomar la 4ta. variable (D) y proceder a invertir los bits impares.} { Se cambia el orden de las tareas para aprovechar que la máscara nos permite obtener D' fácilmente, luego determinaremos A'}
mascara := mascara shl 1;
// 00000000 00000000 00000000 10101010, OK
{ Invertimos los bits impares de D}
d1 := d xor mascara;
// 00000000 00000000 00000000 11101101 xor
// 00000000 00000000 00000000 10101010 = // 00000000 00000000 00000000 01000111 OK { Tomar la 1ra. variable (A) e intercambiar los cuatro bits más significativos con los cuatro menos significativos: Para intercambiar los bits más y menos significativos emplearemos la siguiente regla: 0 or 0 = 0 1 or 0 = 1 ==> Bit or 0 = Bit
78
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
0 or 1 = 1 1 or 1 = 1 ==> Bit or 1 = 1 0 and 0 = 0 1 and 0 = 0 ==> Bit and 0 = 0 0 and 1 = 0 1 and 1 = 1 ==> Bit and 1 = Bit} { Formamos la máscara} mascara := 1; // 00000000 00000000 00000000 00000001 mascara := mascara shl 1; // 00000000 00000000 00000000 00000010 mascara := mascara + 1; // 00000000 00000000 00000000 00000011 mascara := mascara shl 1; // 00000000 00000000 00000000 00000110 mascara := mascara + 1; // 00000000 00000000 00000000 00000111 mascara := mascara shl 1; // 00000000 00000000 00000000 00001110 // 00000000 00000000 00000000 00001111, OK mascara := mascara + 1; // Tomamos los bits menos significativos y borramos los otros a1 := a and mascara; // 00000000 00000000 00000000 00110101 and // 00000000 00000000 00000000 00001111 = // 00000000 00000000 00000000 00000101 { Desplazamos los bits a la izquierda} a1 := a1 shl 4; // 00000000 00000000 00000000 01010000 { Desplazamos los bits de la máscara} mascara := mascara shl 4; // 00000000 00000000 00000000 11110000 { Tomamos los bits más significativos y borramos los otros} // 00000000 00000000 00000000 00110101 and aux := a and mascara; // 00000000 00000000 00000000 11110000 = // 00000000 00000000 00000000 00110000 { Desplazamos los bits a la derecha} aux := aux shr 4; // 00000000 00000000 00000000 00000011 { Juntamos los bits más y menos significativos invertidos} a1 := a1 or aux; // 00000000 00000000 00000000 01010011 { Tomar la 2da. variable (B) e intercambiar los dos bits menos significativos} { Tomamos los dos bits menos significativos} aux := b and 3; // 00000000 00000000 00000000 00110101 and // 00000000 00000000 00000000 00000011 (3) = // 00000000 00000000 00000000 00000001 { Borramos los dos bits menos significativos} mascara := 3; // 00000000 00000000 00000000 00000011 mascara := not mascara; // 11111111 11111111 11111111 11111100 // 00000000 00000000 00000000 00110100 b1 := b and mascara;
79
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
{ Colocamos el 2do. bit en la primera posición} b1 := b1 or (aux shr 1); // 00000000 00000000 00000000 00000001 aux // 00000000 00000000 00000000 00000000 shr 1 // 00000000 00000000 00000000 00110100 b1 // 00000000 00000000 00000000 00110100 b1 or... { Borramos el 2d. bit de la variable auxiliar y movemos el 1ro. a la segunda posición.} mascara := 2; // 00000000 00000000 00000000 00000010 mascara := not mascara; // 11111111 11111111 11111111 11111101 aux := aux and mascara; // 00000000 00000000 00000000 00000001 and // 11111111 11111111 11111111 11111101 = // 00000000 00000000 00000000 00000001 aux := aux shl 1; // 00000000 00000000 00000000 00000010 { Colocamos el bit en la variable b1} // 00000000 00000000 00000000 00110100 or b1 := b1 or aux; // 00000000 00000000 00000000 00000010 = // 00000000 00000000 00000000 00110110 OK { Formar nuevamente el número con los cambios hechos, en el orden como se piden:C'A'D'B'} c1 := c1 shl 24; // 11010000 00000000 00000000 00000000 // 00000000 01010011 00000000 00000000 a1 := a1 shl 16; d1 := d1 shl 8; // 00000000 00000000 01000111 00000000
nuevoValor := c1 or a1 or d1 or b1; // 11010000 01010011 01000111 00110110 writeln('Valor inicial: ',valor:12); writeln('Valor final: ', nuevoValor:12); end.
Introducción al uso de archivos de texto Un archivo de textos es una colección de caracteres almacenados en la memoria secundaria del computador. La idea de emplear archivos de textos es poder almacenar información que podrá ser empleada cada vez que se ejecute el programa o por diferentes programas, esto significa que podemos almacenar en un archivo de textos los datos que vamos a introducir al programa y hacer que el programa los tome de allí sin necesidad que los tengamos que digitar cada vez que queremos ejecutar el programa. La ventaja de emplear archivos de textos es que la consola del computador ha sido diseñada de manera similar a la de un archivo de textos, de modo que introducir datos a un programa desde la consola o desde un archivo de textos es casi lo mismo, las instrucciones que utilizaremos en uno u otro caso serán muy parecidas. Un archivo de textos se puede crear mediante instrucciones de programa, pero la forma más fácil de hacerlo es empleando un procesador de palabras, por lo general se emplea el que se proporciona en el entorno de desarrollo para escribir los programas, pero se puede usar cualquiera. No obstante lo expuesto en los párrafos anteriores, existen algunas diferencias en el ingreso de datos por consola que por un archivo de textos, las cuales analizaremos a continuación: 80
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Variable de archivo: Para poder utilizar un archivo de textos se requiere relacionar de manera lógica el archivo con el programa, la manera de hacerlo es definiendo una variable especial en el programa denominada variable de archivo la cual será conectada al mismo archivo. La sintaxis para declarar una variable de archivo es la siguiente: var
:
Identificador
;
text
,
Por ejemplo: program variableDeArchivo; var archivo: Text; datosDeEntrada, reporte: Text; begin … end. Asignación de un archivo a una variable de archivo: Una vez definida la variable de archivo, hay que asignarle la información necesaria para que la variable se conecte con el archivo físico, esta operación se realiza mediante un procedimiento denominado assign, su sintaxis se muestra a continuación: assign
(
Variable de archivo
,
Nombre del archivo
)
;
Por ejemplo: … begin assign(archivo, ‘datos.txt’ ); {el nombre del archivo debe estar entre comillas simples} assign(reporte, ‘c:\ejemplos\reporte.txt’); {también se puede colocar la ruta} … Apertura de archivos: Luego de conectar la variable de archivo, debemos realizar una operación que nos permita poder acceder a la información que tiene el archivo o, en el caso que queramos guardar datos en un archivo, que nos permita poder escribir la información requerida en el archivo. Esta operación se denomina apertura del archivo, debe entenderse que esta operación no lee ni escribe información, solo da las condiciones necesarias para poder realizarlas. La apertura de un archivo de textos implica el establecer el tipo de operación que se realizará con el archivo, esto es, establecer si se leerá del archivo la información que hay en él o si se escribirá información en él. En los archivos de textos estas tareas son excluyentes, esto quiere decir que se abre un archivo sólo para leer información de él o 81
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
sólo para escribir en él, no se puede abrir un archivo de textos para realizar ambas operaciones, como se hacen con otro tipo de archivos. Estas operaciones se realizan mediante tres procedimientos diferentes, los que detallamos a continuación: 1° Procedimiento reset Este procedimiento permite abrir un archivo de modo que se puedan realizar sólo operaciones de lectura a partir del inicio del archivo. El archivo debe existir de lo contrario se producirá un error y se interrumpirá la ejecución del programa. La sintaxis de este procedimiento es la siguiente: reset
(
Variable de archivo
)
;
Por ejemplo: … begin … reset(archivo); … 2° Procedimiento rewrite Rewrite permite abrir un archivo de modo que se puedan realizar sólo operaciones de escritura a partir del inicio del archivo. Si el archivo no existe lo crea, de lo contrario borra el contenido del archivo. La sintaxis de este procedimiento es la siguiente: rewrite
(
Variable de archivo
)
;
Por ejemplo: … begin … rewrite(reporte); … 3° Procedimiento append Permite abrir un archivo de modo que se puedan realizar sólo operaciones de escritura a partir del final del archivo, en otras palabras nos permite agregar información a un archivo. El archivo debe existir, de lo contrario se produce un error y se interrumpirá la ejecución del programa. La sintaxis de este procedimiento es la siguiente: append
(
Variable de archivo
)
;
Por ejemplo: 82
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
… begin … append(datosdeEntrada); … Cierre de un archivo Inmediatamente se termine de realizar operaciones con un archivo, éste debe cerrarse. De no hacerse se podría perder información importante del archivo. La razón de esto es que cuando se realizan operaciones de entrada o salida de un archivo, la operación no desplaza la información directamente desde el archivo hacia las variables o viceversa, lo que sucede es que en el momento de abrir el archivo para leer de él, parte de la información contenida en el archivo pasa a un espacio temporal de memoria denominado ‘buffer’, cuando se hace una operación de lectura se toman los datos del buffer y no del archivo, cuando toda la información del buffer se ha leído, nuevamente se le carga otra parte del archivo y el proceso continua. Cuando se escribe en un archivo, la información se envía al buffer, cuando el buffer se llena, se descarga la información que contiene al archivo. Si el programa se interrumpiera abruptamente antes de cerrar el archivo, el contenido del buffer no pasaría al archivo, y por lo tanto se perdería la información que contiene. Esto es un suceso crítico en operaciones de escritura, ya que cuando se lee, lo que hay en el buffer es una copia de lo que hay en el archivo así que si se interrumpiera al programa es difícil que se pierda información del archivo. El esquema siguiente muestra este proceso: Operaciones de escritura
variables
disco
buffer Memoria principal
variables
buffer
disco
Operaciones de lectura
Memoria principal
Esta forma de manejar los datos se da para evitar demasiadas operaciones directamente sobre el disco, de no hacerlo así se perdería mucho tiempo en el proceso ya que el disco es un dispositivo mucho más lento en sus procesos que la memoria principal del computador, por otro lado se evita un desgaste innecesario del disco. Luego de cerrar un archivo, se puede volver a abrir empleando cualquiera de los procedimientos explicados anteriormente, sin necesidad de volver a ejecutar la orden assign. Para cerrar un archivo se emplea el procedimiento close, cuya sintaxis se muestra a continuación. close
(
Variable de archivo
)
;
83
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Por ejemplo: … begin … close(datosdeEntrada); … Entrada y Salida de datos desde archivos de texto Una vez abierto el archivo, se podrán realizar operaciones de escritura o de lectura, según como fue abierto, y estas operaciones se tornan muy sencillas ya que para hacerlo se emplean los mismos procedimientos que se emplean para leer o escribir datos en la consola, esto es: read, readln, write y writeln. Estos procedimientos sólo tienen una variante cuando se trata de archivos de textos, se debe colocar como primer parámetro la variable de archivo relacionada al archivo con el que queremos trabajar, el resto de la sintaxis, así como la forma como trabaja es idéntico a la forma como se trabaja desde la consola. La sintaxis de estos procedimientos se muestra a continuación: write/writeln
(
variable de archivo
expresión
)
;
tamaño de campo , read/readln
(
variable de archivo
variable
)
;
,
Por ejemplo: … begin … read(archivo, base, altura); writeln(reporte, ‘Área = ‘, base*altura/2); …
Ejemplos de programas que emplean archivos de textos: A continuación presentamos algunos ejemplos sencillos de problemas que se solucionarán leyendo los datos de un archivo de textos o guardando los resultados en otro archivo de textos. 1. Escriba un programa en Pascal que permita leer las coordenadas de dos puntos en un plano y que calcule la distancia entre ellos y el ángulo que forma el segmento de recta con la horizontal. Los datos deberán ser leídos de un archivo de textos. 84
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Solución: Como se trata del mismo ejemplo que presentamos en el acápite anterior, nos centraremos en la parte de la lectura de datos, que se realizará desde un archivo de textos. Preparación de los datos: Primero vamos a crear varios archivos de textos en donde pondremos diferentes juegos de datos que serán la fuente de ingreso. Para realizar esto hay que cargar en el computador un editor de palabras como el bloc de notas de Microsoft®, o el mismo editor de textos donde escribe sus programas. Luego escriba en ellos un juego de datos de la manera siguiente:
ó
Luego guárdelo con el nombre de datos-t1.txt en la misma carpeta donde está escribiendo sus programas. De igual manera cree otros dos archivos de textos (datos-t2.txt y datos-t3.txt) con juegos de datos diferentes como por ejemplo 50, 50, 200, 200 y 184, 56, 25, 159. Programa: El programa que presentamos a continuación es el mismo que presentamos en el ejemplo del acápite anterior, sólo le agregamos las instruccionenes referidas a los archivos de textos (recuadros): Program calculaLaDistanciaYAnguloEntreDosPuntos; var x1, x2, y1, y2, d, ang: Real; nombreDelArchivo: String; Variable de archivo archDatos: Text; begin {Lectura de datos:} write(‘Ingrese el nombre del archivo que contiene los datos: ‘); readln(nombreDelArchivo); Asignación, apertura assign(archDatos, nombreDelArchivo); para leer y lectura de reset(archDatos); datos del archivo. readln(archDatos, x2, y2); Observe que se eliminan los readln(archDatos, x2, y2);
mensajes para el ingreso de las coordenadas
{Calculamos resultados:} d := sqrt( sqr(x2-x1) + sqr(y2-y1) ); ang := arcTan( (y2-y1) / (x2-x1) ) * 180 / pi; {Mostramos resultados:} writeln(‘La distancia entre P1 y P2 es: ‘, D:7:3); writeln(‘EL ángulo que forma el segmento con la horizontal es: ‘, ang:7:2, ‘°’); close(archDatos); end.
85
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Resultado: Al ejecutar el programa, primero le pedirá el nombre del archivo que contiene los datos, usted deberá escribir el nombre completo del archivo (p. e.: datos-t1.txt), si el archivo de datos no se encuentra en la misma carpeta que la del programa, deberá colocar la ruta completa para poderlo ubicar (p. e.: c:\ejercicios\tp\datos-t1.txt). Inmediatamente después de haber ingresado el nombre verá los resultados en pantalla como se ve a continuación:
Usted podrá ejecutar el programa varias veces ingresando los nombres de los otros archivos de textos y obtendrá los resultados para esos datos. 2. Escriba un programa que permita ingresar el nombre de un alumno, su código y sus notas de un curso (cuatro prácticas, las notas de los exámenes 1, 2 y del examen especial). El programa deberá calcular la nota que obtendrá el alumno en el curso de manera similar al ejemplo del acápite anterior. Los datos deberán ser leídos de un archivo de textos, los resultados también serán guardados en otro archivo de textos. Solución: de manera similar al programa anterior, nos concentraremos en la lectura de los datos y la emisión del reporte con los resultados. Datos: Prepare tres archivos de textos con los siguientes datos: PaulaRod.txt 20040107 Paula Rodríguez 18 15 17 16 15 18 -1
AnaRonc.txt 20051101 Ana Roncal 13 12 14 15 11 -1 15
NaomiGuz.txt 20071003 Naomi Guzmán 15 9 15 17 -1 16 18
Programa: El programa que presentamos a continuación es el mismo que presentamos en el ejemplo del acápite anterior, sólo le agregamos las instruccionenes referidas a los archivos de textos (recuadros): program promedioDeNotas; var pr1, pr2, pr3, pr4, ex1, ex2, exE, prMin, nota: Integer; promPr : Real; nombre, codigo, nombArchDatos, nombArchReporte: String; archDatos, archReporte: Text; begin 86
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
{Lectura de datos:} write('Ingrese el nombre del archivo con los datos: '); readln(nombArchDatos); write('Ingrese el nombre del archivo que guardará el reporte: '); readln(nombArchReporte); assign(archDatos, nombArchDatos); assign(archReporte, nombArchReporte); reset(archDatos); rewrite(archReporte); {crea un nuevo archivo} readln(archDatos, codigo); readln(archDatos, nombre); readln(archDatos, pr1, pr2, pr3, pr4); readln(archDatos, ex1, ex2, exE); {Cálculos:} {1) Tomamos la menor entre pr1 y pr2} prMin := ord(pr1<=pr2)*pr1 + ord(pr2-1)*ex1 + ord((ex1=-1) and (ex2<>-1))*exE; {Decidimos reemplazar o no el examen especial con el examen 2} ex2 := ord(ex2<>-1)*ex2 + ord(ex2=-1)*exE; nota := round((promPr*3 + ex1*3 + ex2*4) / 10); {Mostrar resultados:} writeln(archReporte, 'EL alumno: ', codigo, ' ', nombre, ' obtuvo de nota: ' , nota); close(archDatos); close(archReporte); end. Resultado: Ejecute el programa tres veces, dándole los nombres de los diferentes archivos de texto que creó. En cada ejecución dele un nombre diferente al archivo que contendrá el reporte, luego abra los reportes y obsérvelos. Vuelva a ejecutar tres veces el programa, dele en cada ejecución el nombre de cada uno de los archivos de datos, pero cuando le pida el nombre del archivo del reporte dele el mismo nombre en las tres ejecuciones. Cuando abra el archivo de reporte verá que en él está la repuesta sólo de la última ejecución, esto se debe a que la orden rewrite borra el contenido del archivo al abrirlo. Por último cree un archivo de textos con el nombre de reporte.txt que esté vacío, es decir que no contenga información alguna. Luego reemplace, en el programa, la línea que 87
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
dice rewrite(archReporte); por la orden append(archReporte); finalmente vuelva a ejecutar tres veces el programa dándole como nombre para el archivo de reporte el que creo vacío, podrá observar que el contenido del archivo será similar al que se muestra a continuación. reporte.txt EL alumno: 20040107 Paula Rodríguez obtuvo de nota: 17 EL alumno: 20051101 Ana Roncal obtuvo de nota: 13 EL alumno: 20071003 Naomi Guzmán obtuvo de nota: 17
88
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 5: Programación estructurada Cuando en 1958 aparece el primer lenguaje de programación de alto nivel, el FORTRAN (del acrónimo FORmula TRANslation) y posteriormente el COBOL (COmmon BusinessOriented Language) en año 1960, la programación de computadoras se empezó a popularizar. Cada vez, más y más personas se introducía en el mundo de la programación de computadores y sobre todo cada vez se pretendía desarrolla programas más y más complejos. Sin embargo, las características de estos lenguajes, aun muy cerca a la manera cómo trabaja el computador y muy alejada de la forma cómo se comunican y expresan las personas, hizo que los programas desarrollados en esa época no fueran muy claros y por el contrario eran altamente desordenados. Entre otras, existía una orden en los lenguajes denominada GOTO, esta polémica instrucción hacía que en el momento de ejecutarse se transfería el control del programa a una línea que no necesariamente fuera la que estuviera a continuación. En otras palabras, si la orden “goto 100” se encontraba en la línea 15, al ejecutarse, la siguiente orden que se ejecutaría en el programa sería la que se encontrase en la línea 100 o que tuviera como etiqueta es número. El esquema No.1 muestra cómo trabajaba esta instrucción. Programa
Flujo del programa Se ejecutan todas las instrucciones hasta la que tiene el GOTO.
GOTO 100
Se transfiere el control del programa a la instrucción con etiqueta 100. No se ejecutan las instrucciones intermedias.
100 Se continúa con la ejecución de todas las instrucciones restantes.
Esquema No.1
END
El uso indiscriminado que se le dio a esta instrucción trajo consigo la proliferación de programas muy difíciles de mantener, actualizar o simplemente corregir. Para que pueda darse cuenta de esto, si observa el esquema anterior podrá ver que un grupo de instrucciones no se ejecuta en el programa, para que se puedan ejecutar habrá que poner otra sentencia GOTO que transfiera el control del programa a esa zona, por ejemplo se puede poner una instrucción GOTO 50 antes de la sentencia END. Pero al hacer esto, la ejecución del programa seguirá línea a línea desde la sentencia que tienen la etiqueta 50 pasando por la que tiene la etiqueta 100 las cuales se volverán a ejecutar. Pero esto no es lo más grave, ya que cuando se llegue a ejecutar nuevamente la sentencia GOTO 50 el control del programa volverá a la línea que tiene la etiqueta 50, repitiéndose el proceso indefinidamente, tal como se muestra en el esquema No. 2.
89
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo Programa
Flujo del programa
GOTO 100 50
100
GOTO 50 END
Esquema No.2
Para solucionar esto se debía colocar otra sentencia GOTO una línea antes de la que contiene la etiqueta 100, que transfiera el control a la sentencia END, como se muestra en el esquema No. 3. Programa
Flujo del programa
GOTO 100 50
GOTO 200 100
Esquema No.3
GOTO 50 200 END
Durante esa época se llegó a extremos en los que cuando se encontraban errores en un programa no se tomaban la molestia de borra o modificar las líneas erróneas, sino que se hacía uso de la orden GOTO como se muestra en el esquema No. 4. Programa
Flujo del programa
GOTO 200
100
150 END 200
Esquema No.4
=
A fines de los años 60 la programación de computadoras se había vuelto caótica, cada quien programaba como mejor le parecía, muchas veces era mejor rehacer un programa desde cero en vez de tener que corregirlo o actualizarlo. Por esa época aparecen estudiosos de la computación que se dedicaron a analizar los problemas que existían en este sentido, uno de los más famosos fue el artículo publicado por Edsger W. Dijkstra, “Go To Statement Considered Harmful”. 90
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Por el año 1966, los científicos Corrado Böhm y Giuseppe Jacopini formularon un teorema, que hoy lleva sus nombres, por medio del cual se sentó las bases de lo que hoy en día se conoce como “Programación estructurada”. El Teorema de Böhm-Jacopini, en forma simplificada nos dice que “Todo algoritmo propio puede expresarse en base a tres estructuras elementales que son la estructura secuencial, la estructura selectiva y la estructura iterativa. Estas estructuras se pueden representar mediante los siguientes diagramas de flujo: Estructura Secuencial
Estructura Selectiva
Estructura Iterativa
Estas estructuras están formadas por un conjunto de instrucciones que forman un bloque de sentencias, como se puede apreciar en las líneas punteadas del gráfico anterior. Dentro de las consideraciones dadas en la programación estructurada se indica que estos bloques deben tener un único punto de entrada y un único punto de salida, prohibiéndose la posibilidad de interrumpir la ejecución del bloque en un punto intermedio o de poder llegar a una instrucción del bloque por un camino que no pase por el punto de entrada. Esto eliminó la posibilidad de emplear sentencias como GOTO lo que ordenó la manera de programar. Es a partir de esto que empiezan a aparecer lenguajes de programación que se adaptaron a esta nueva forma de programación. Es así como aparecen el Algol, el Pascal, el C, el Ada, etc.
Estructura Secuencial El efecto de una estructura secuencial se aprecia cuando se encuentran una serie de sentencias que se ejecutan una a continuación que la otra desde la primera hasta la última. Todos los programas que se desarrollaron en el capítulo anterior se basan en una estructura secuencial. Es por esto que se considera la estructura más simple de las tres, por esto no desarrollaremos más ejemplos en este sentido y más bien pasaremos a explicar las otras estructuras. Estructura Selectiva La estructura selectiva nos permite elegir entre dos caminos, mutuamente excluyentes, en el flujo de un programa. Para esto, la instrucción que implementa esta estructura, como primer paso debe evaluar una expresión que en la mayoría de casos es una 91
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
expresión lógica (sin embargo en lenguajes como el C simplemente se evalúa una expresión). El resultado de esta expresión nos indicará cuál es el camino que seguirá el flujo del programa, si el resultado es “verdadero” o “true” se seguirá por un camino, en caso contrario si el resultado es “falso” o “false” se seguirá por el otro camino. En el caso del C el camino se decide si el resultado es cero (que equivale al “falso”) o diferente de cero. En Pascal la estructura selectiva se implementa mediante la instrucción denominada IF…THEN…ELSE, cuya sintaxis se presenta a continuación: if
Expresión lógica
then
Bloque de Instrucciones else
Bloque de Instrucciones
Un camino en esta instrucción se sigue al ejecutar el bloque de instrucciones que está a continuación de la palabra reservada then, que se toma en el caso que el resultado de la expresión lógica sea true, y el otro camino está a continuación de la palabra reservada else. Si observa la sintaxis verá que tanto la palabra reservada else como el bloque de instrucciones que le siguen están en una zona opcional dentro de la instrucción, eso quiere decir que si se desea se les puede obviar, por lo tanto la instrucción IF…THEN…ELSE también puede servir para que en un programa se opte por ejecutar un grupo de instrucciones o no. Algunos ejemplos de cómo emplear esta instrucción se muestran a continuación: if a > b then writeln (“Error en los datos”); Aquí, si el valor que contiene la variable a es por ejemplo 3 y el valor que contiene b fuera de 1, entonces al ejecutarse la orden anterior será verdadero que a es mayor que b por lo tanto se mostrará en la pantalla el mensaje “Error en datos”, pero si por el contrario la variable a tuviera 3 y la variable b tuviera 8 no se ejecutaría la orden writeln y se continuaría ejecutando las siguientes instrucciones que le siguen al if en el programa. if a > b then writeln (“Error en los datos”) else c := sqrt(b-a); En esta orden se trata de calcular la raíz cuadrada de b-a, sin embargo sabemos que si se intenta obtener la raíz cuadrada de un número negativo se produce un error en el programa y se interrumpe éste. Para evitar esto colocamos una orden condicional de modo que se seleccione camino a seguir dependiendo de los valores de las variables, de este modo se evita ejecutar una orden que produzca un error crítico en el programa. if a > b then writeln (“Error en los datos”) else begin c := sqrt(b-a); writeln(“El resultado es: “, c:8:2); end; 92
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Este caso es similar al anterior, pero aquí se quiere ejecutar dos instrucciones para el caso que a no sea mayor que b. Las cláusulas then y else sólo pueden influenciar sólo a la instrucción que le sigue por eso para que su influencia llegue a más de una sentencia, éstas deben ser enmarcadas en un bloque que comience con la palabra reservada begin y que termine con la palabra reservada end como se ve en el ejemplo anterior. Así, se puede escribir una porción de código como en el ejemplo siguiente: if a > b then begin writeln (“Error: a debe ser menor que b, ingréselos nuevamente:”); readln(a, b); end else begin c := sqrt(b-a); writeln(“El resultado es: “, c:8:2); end;
Ejemplos de programas que emplean la estructura selectiva: 1. Se desea escribir un programa que permita calcular cuánto debe pagar por impuesto a la renta un ciudadano según la cantidad de dinero que ganó en un año. El impuesto se debe calcular según la siguiente tabla: Renta Neta Global Hasta 27 UIT Por el exceso de 27 UIT y hasta 54 UIT Por el exceso de 54 UIT
Tasa 15% 21% 30%
Sabiendo que este año una UIT tiene un valor de S/. 3,550.00 Solución: Este es un problema típico en el que se emplean estructuras selectivas en su solución. Sin embargo la solución que se pueda plantear no es única, se puede establecer varios caminos con los que se obtengan la misma respuesta. Para que se pueda apreciar esto, se planteará dos formas de presentar el programa que de solución al problema planteado, el primero se empleará varias instrucciones IF sin la cláusula ELSE, y en la segunda se empleará una técnica que se denomina de “anidación”. Datos: Se ingresará como dato la cantidad de dinero en nuevos soles que un ciudadano ganó en un año. El valor de la UIT es una cantidad que no cambia muy a menudo, por lo general cambia una vez al año, por lo que no tiene sentido que lo ingresemos como dato cada vez que se ingrese el monto ganado por un ciudadano por lo tanto lo tomaremos como una constante del programa, cuando este valor cambie tendremos que modificar el valor en el programa y compilarlo nuevamente 13. Otros datos que se deben considerar son los valores de los porcentajes y los límites dados, estos datos 13
Otra solución puede ser colocar el valor de la UIT en un archivo de texto y leerlo cada vez que se ejecute el programa, así que cada vez que cambie su valor, sólo tendríamos que modificar el archivo. Esta solución queda como ejercicio para el lector.
93
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
son fijos y se podrían manejar como valores constantes, pero podrían cambiar en algún momento, por esto también los tomaremos como constantes. Resultado: El impuesto que debe pagar el ciudadano. Tareas que se han de realizar en el programa: - Ingresar la cantidad ganada en el año. - Verificar si la cantidad es menor o igual a 27 UIT, si es así el impuesto será el 15% de la cantidad ingresada. - Verificar si la cantidad está entere 27 UIT y 54 UIT, si es así el impuesto se calculará, ya que se indica “por exceso de..”, de la siguiente manera: 27 UIT x 15% + (la cantidad – 27 UIT) x 21% - Verificar si la cantidad es mayor que 54 UIT, si es así el impuesto se calculará como a continuación se indica: 27 UIT x 15% + (54 UIT – 27 UIT) x 21% + (la cantidad – 54 UIT) x 30% Programa: Como se indicó se presentarán dos programas que solucionene el problema. 1) program calculaElImpuestoALaRentaDeUnCiudadano; {Esta solución se presenta empleando la instrucción if varias veces sin emplear la cláusula else} const UIT = 3550.0; {se debe tomar como un valor real} PORCENT_1 = 15.0; LIMITE_1 = 27.0; PORCENT_2 = 21.0; LIMITE_2 = 54.0; PORCENT_3 = 30.0; var cantidadGanada, impuesto: Real; begin write('Ingresa la cantidad que ganó en el año: '); readln(cantidadGanada); if cantidadGanada <= LIMITE_1 * UIT then impuesto := cantidadGanada * PORCENT_1/100; if (cantidadGanada > LIMITE_1 * UIT) and (cantidadGanada <= LIMITE_2 * UIT) then impuesto := LIMITE_1 * UIT * PORCENT_1/100 + (cantidadGanada - LIMITE_1 * UIT) * PORCENT_2/100; if cantidadGanada > LIMITE_2 * UIT then impuesto := LIMITE_1 * UIT * PORCENT_1/100 + (LIMITE_2 - LIMITE_1) * UIT * PORCENT_2/100+ (cantidadGanada - LIMITE_2 * UIT) * PORCENT_3/100; { Observe que de las tres instrucciones if que aparecen, sólo una expresión lógica dará verdadero, por lo tanto una sola vez se calcular el impuesto.} writeln( 'El ciudadano debe pagar: S/. ', impuesto:10:2, 'de impuesto a la renta.'); end. 94
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
2) program calculaElImpuestoALaRentaDeUnCiudadano; { Esta solución se presenta empleando la instrucción if en forma anidada} const UIT = 3550.0; {se debe tomar como un valor real} PORCENT_1 = 15.0; LIMITE_1 = 27.0; PORCENT_2 = 21.0; LIMITE_2 = 54.0; PORCENT_3 = 30.0; var cantidadGanada, impuesto : Real; begin write('Ingresa la cantidad que ganó en el año: '); readln(cantidadGanada); if cantidadGanada <= LIMITE_1 * UIT then impuesto := cantidadGanada * PORCENT_1/100 else if cantidadGanada <= LIMITE_2 * UIT then impuesto := LIMITE_1 * UIT * PORCENT_1/100 + (cantidadGanada - LIMITE_1 * UIT) * PORCENT_2/100 else impuesto := LIMITE_1 * UIT * PORCENT_1/100 + (LIMITE_2 - LIMITE_1) * UIT * PORCENT_2/100+ (cantidadGanada - LIMITE_2 * UIT) * PORCENT_3/100; { Aquí se observa lo que se denomina un if anidado (un if dentro de otro if). Si la cantidad es menor o igual al primer límite, se calcula el impuesto con la primera fórmula y no se ejecutan las sentencias que le siguen al else. Por el contrario si la cantidad es mayor que ese límite se ejecuta el segundo if, aquí las expresiones lógicas se simplifican debido a que si se llega a ese punto la cantidad no puede ser menor que el primer límite.} writeln('El ciudadano debe pagar: S/. ', impuesto:10:2, ' de impuesto a la renta.');
end.
2. Escriba un programa que permita hallar las raíces de una ecuación de segundo grado, como se muestra a continuación: ax2 + bx + c = 0 Datos: Se ingresará como dato los coeficientes a, b y c. Resultados: Se deberá obtener, de ser posible, ambas raíces de la ecuación, sea el valor que sea. Solución: Apliquemos una tormenta de ideas para determinar la solución del problema. Lo primero que se nos viene a la mente cuando vemos la ecuación de segundo grado es la fórmula para determinar sus raíces y esta es: raíces (x , x ) = − b ± b − 4ac 2
1
2
2a
Si es cierto que esta fórmula nos da las raíces de la ecuación, no se pude aplicar siempre, por un lado si el valor del coeficiente “a” fuera cero no se puede hacer la división, y por otro lado si el radical b 2 − 4ac nos diera un valor negativo, la raíz cuadrada nos daría un valor complejo o imaginario, y muchos lenguajes de 95
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
programación no soportan el manejo de números complejos, por lo que debemos analizar estos casos para dar una respuesta adecuada. Si la variable “a” tiene un valor cero, la ecuación quedaría de la siguiente manera: 0
ax2 + bx + c =0 bx + c = 0 Por lo tanto nos encontramos en el caso que la ecuación no es de segundo grado, la raíz por consiguiente será
raíz (x ) = −
c b
.
Aquí encontramos la posibilidad que también
la variable “b” tenga un valor de cero cuando simultáneamente la variable “a” tiene el valor de cero, en este caso nos encontraríamos ante la expresión c = 0, la cual no es una ecuación, por lo tanto no se puede determinar una raíz. En el caso que el valor de la variable “a” no sea cero y el radical b 2 − 4ac sea negativo, como se indicó, las raíces serían valores complejos y el resultado se tiene 2 que expresar en estos términos: raices (x 1 , x 2 ) = − b ± 4ac − b i 2a
2a
Esta solución, producto de una tormenta de ideas da solución al problema, pero es muy desordenada, lo que viene ahora es ordenar estas ideas de modo que podamos encajarlo en un programa claro y sencillo. Se empezará verificando el valor de la variable “a”, según sea el caso se verificará el valor de la variable “b” o del radical para dar una respuesta en el programa. Programa: El programa resultante se muestra a continuación: program raicesDeEcuacionDe2doGrado; var a, b, c, radical, raiz1, raiz2, parteReal, parteImag: Real; begin write(‘Ingrese los coeficientes a, b, c: ’); read(a, b, c); if a = 0 then begin if b = 0 then writeln(‘No es una ecuación de segundo grado’ ) else begin writeln(‘La ecuación es de primer grado:’); raiz1 := -c/b; writeln(‘X = ’, raiz1:8:3); end end else begin radical := b*b – 4*a*c; if radical >= 0 then begin raiz1 := (-b + sqrt(radical))/2/a; raiz2 := (-b - sqrt(radical))/2/a; writeln( ‘X1 = ‘, raiz1:8:3, #13, #10, ‘X2 = ‘, raiz2:8:3); end else begin parteReal := -b/2/a; parteImag := sqrt(-radical)/2/abs(a); 96
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
end end.
writeln( ‘X1 = ‘, parteReal:8:3,’ +’, parteImag:8:3,’i’); writeln( ‘X2 = ‘, parteReal:8:3,’ -’, parteImag:8:3,’i’); end
Estructura Iterativa Con la estructura iterativa se busca repetir la ejecución de un grupo de instrucciones varias veces. Las iteraciones en los programas es algo muy común, casi todos los algoritmos requieren de la repetición de la ejecución de grupos de instrucciones para conseguir sus objetivos, sin embargo se debe tener en cuenta que un algoritmo no puede aceptar que un grupo de instrucciones se ejecute de manera repetitiva un número infinito de veces, toda iteración debe llegar a un fin en algún momento. Por lo tanto cuando se itera en un programa se debe colocar un control que permita dar por terminada la iteración en algún momento. Este control se puede colocar al inicio de las sentencias que se iterarán, en cuyo caso se le denominará “iteración o bucle con entrada controlada”, o se puede colocar al final, denominándosele “iteración o bucle con salida controlada”. La figura siguiente muestra estos conceptos: Bucle con entrada controlada
Bucle con salida controlada
También, en los diferentes lenguajes de programación se implementan otras formas de iteración, como aquellas que controlan la salida mediante un contador, en cada iteración se incrementa o decrementa un índice, cuando ese índice llega a un límite se terminas la iteración. Las iteraciones también se pueden controlar en un punto intermedio del bloque de instrucciones, esto es una práctica común sin embargo no se conlleva con los lineamientos de la programación estructurada. A pesar de esto, en algunos casos esta forma de iterar puede hacer más claro el código de un programa por lo que lo presentaremos más adelante. A continuación se describen las diferentes formas en que se presentan las estructuras iterativas: 97
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Bucle con entrada controlada Esta estructura se implementa en la mayoría de lenguajes de programación mediante una instrucción denominada WHILE…. La idea es que mientras una condición que es evaluada al inicio de un bloque de instrucciones dé cómo resultado un valor verdadero, el boque de instrucciones se volverá a ejecutar, en el momento que el resultado de la expresión dé un valor falso se dará como finalizada la iteración. En Pascal, la sintaxis de la instrucción WHILE… es como a continuación se muestra: while
Expresión lógica
do
Bloque de Instrucciones
Un ejemplo de cómo emplear esta instrucción se muestra a continuación: {cálculo de los diez primeros valores enteros } entero := 0; suma := 0; while entero < 10 do begin inc(entero); suma := suma + entero; end; Allí se puede apreciar cómo se debe inicializar las variables que intervienen en la condición que controla la salida del bucle, de modo que la condición se cumpla y así se pueda empezar a iterar. En este caso se asigna el valor de cero a la variable entero de modo que la expresión entero < 10 de cómo resultado verdadero. Luego, dentro de bloque de instrucciones, se deben modificar los valores de las variables que intervienen en la expresión de control de modo que la iteración llegue a su fin en algún momento, en este caso se coloca la instrucción inc(entero).
Ejemplos de programas que emplean la instrucción while: 1. A usted le regalan una pareja de conejos recién nacidos (una hembra y un macho). Los conejos sólo pueden aparearse cuando llegan a ser adultos, y esto se da cuando cumplen un mes de vida. Si el tiempo de gestación es de 1 mes, recién al final del segundo mes la hembra puede parir nuevos conejos. Suponiendo que los conejos no mueren y cada hebra produce una pareja de conejos (una hembra y un macho) cada mes a partir de su segundo mes de vida. ¿Cuántas parejas de conejos tendremos al final del primer año?, ¿Cuántas a los 6 meses?, etc.… Datos: Se ingresará como dato el número de meses para los que se quiere hacer el control. Resultados: Se deberá obtenerla cantidad de parejas que se tendrá al final del período. 98
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Solución: En la figura siguiente se puede apreciar cómo irá creciendo la población de conejos en términos de parejas. Allí se aprecia con una letra P de color rojo, las parejas recién nacidas o que no han llegado a ser adultas, y en color verde las parejas adultas. Las flechas azules indican el nacimiento de una pareja de conejos
P
P
P
P
P
P
P
P
Mes 0
1 pareja
P
Mes 1
1 pareja
P
P
Mes 2
2 parejas
P
P
Mes 3
3 parejas
P
P
P
Mes 4
5 parejas
P
P
Mes 5
8 parejas
P
P
Note que la cantidad de conejos se incrementa siguiendo las reglas de la famosa serie de Fibonacci. Un término de esta serie se calcula sumando los dos términos anteriores al que se quiere calcular, esto es fibonacci(n) = fibonacci(n-1) + fibonacci(n-2). De acuerdo a esto se tiene que: Mes 0 1 2 3 4 5 6 7 8 9 10 11 … Número de parejas 1 1 2 3 5 8 13 21 34 55 89 144 … El algoritmo que se emplee para solucionar este problema debe contemplar el hecho que no se puede definir una variable por cada término de la serie, en primer lugar porque el usuario puede solicitar cualquier término de la serie. Lo que se hace en estos casos es tratar de manejar un número pequeño de variables que guarden los últimos valores de la serie en un momento determinado, esto debido a que un término de la serie se calcula en función de los dos términos anteriores. En otras palabras si tenemos que para el mes 0 el valor de la serie es 1 (M (0) = 1) y para el mese 1 el valor de la serie es 1 (M (1) = 1) podemos calcular el valor de la serie para el mes 2 como M(2) = M(1) + M(0) = 2. Para el mes 3 no se requiere del mes 0 ya que M(3) = M(2) + M(1) = 2 + 1 = 3 Para el mes 4 no se requiere del mes 0 ni del mes 1: M(4) = M(3) + M(2) = 3 + 2 = 3 Y así sucesivamente… Por lo tanto sólo se requiere de tres variables para determinar un valor de la serie, sea cual sea el término que se desea calcular. La razón para esta conjetura es que una variable se puede inicializar con el primer término de la serie, la segunda con el segundo término de la serie, la tercera variable servirá para calcular el siguiente término de la serie (en este caso el tercero), esto es sumando la primera con la 99
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
segunda variable. Para calcular el siguiente término de la serie (el cuarto), podemos pasar el segundo término de la serie a la primera variable (ya que no nos interesa el primer término de la serie) y el termino tres calculado, lo pasamos a la segunda variable, luego el cuarto término de la serie lo calculamos sumando la primera con la segunda variable y lo almacenamos en la tercera variable. Nótese que en este punto el cálculo de la serie ya se vuelve monótono, esto es, se pasa el contenido de la segunda variable a la primera y el contenido de la tercera en la segunda variable y se asigna a la tercera la suma de las otras dos. Esto es una iteración controlada por el número de meses o términos que se desea calcular en la serie. A continuación se muestra este algoritmo en un programa. Programa: El programa resultante se muestra a continuación: program serieFibonacci; {o número de parejas de conejos} var fib, fib_2, fib_1, mes, numMeses: Longint; begin write('Ingrese el número de meses: '); readln(numMeses); if numMeses < 2 then writeln('El número de parejas es: 1') else begin fib_1 := 1; {inicializamos las variables} fib_2 := 1; mes:=2; while mes <= numMeses do begin fib := fib_2 + fib_1; fib_1 := fib_2; fib_2 := fib; inc(mes); end; writeln('El número de parejas es: ', fib); end end. 2. Escriba un programa que permita imprimir un número, dado en base 10, en otra base mayor o igual a 2. Datos: Se ingresará como dato un número en base 10 y la base en la que se quiere convertir el número. Resultados: Se imprimirá el número dado en la base solicitada de una manera sencilla, que se pueda entender fácilmente sin que el usuario deba realizar algún proceso. Solución: En el párrafo anterior se enfatiza que el resultado no debe llevar al usuario tratar de interpretar el resultado, esto es que no debe realizar ningún proceso para entender el resultado. La razón para esto es que la solución no puede ir por el lado fácil para el programador, se sabe que para cambiar un número a otra base sólo hay que dividir el número sucesivas veces entre la base, e ir tomando cada residuo de la división, estos residuos son las cifras del número en la base deseada. Sin embargo si aplicamos este algoritmo en un programa el resultado será la impresión del número al 100
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
revés. Por otro lado, al igual que en el problema anterior, no se puede pretender guardar cada cifra en una variable porque no se sabe cuántas cifras tendrá la solución. Finalmente cuando la base solicitada fuese mayor a 10, los valores de los residuos obtenidos en cada división pueden ser mayores que 9 por lo que el programa los debe convertir a caracteres (10 A, 11 B, etc.). Para obtener las cifras en el orden correcto, se empezará por determinar un factor que sea el mayor número que sea una potencia de la base, pero que no sea mayor que el número que se quiere convertir. Por ejemplo si se quiere convertir el número 247 a la base 2, se determinan las potencias de 2: 1, 2, 4, 8, 16, 32, 64, 128, 256, etc. y se toma el valor 128 como factor. Luego se divide el número entre el factor (247 128) obteniéndose como cociente 1 y como residuo 119. El cociente es la primera cifra del número 247 en base 2. A continuación se toma el residuo y se divide entre la potencia de 2 inmediatamente inferior a la que determinamos como factor, en este caso 64. El cociente será la segunda cifra que buscamos. El proceso se repite hasta que se tome como factor el valor de 1. Al final se obtiene el numero 1110111. En el caso que se elija una base mayor a 10 el proceso es el mismo, con la salvedad que los cocientes podrán salir mayores a 9 por lo que se debe reemplazar este valor por una letra. Por ejemplo si el número 247 se quiere convertir a base 16, se determina como factor (1, 16, 256, etc.) el número 16, al dividirlo el número entre este factor se obtiene como cociente 15 y como residuo 7, por lo que el 15 debe convertirse a la letra F. Finalmente se obtendrá como resultado el valor F7. Programa: El programa resultante se muestra a continuación: program cambioDeBase; var num, factor: Longint; base, cifra: Integer; begin write ('Ingrese un número en base 10: '); readln (num); {Ingresamos la base verificando que ésta sea mayor o igual a 2}
base := 0; while base <2 do begin write ('La base a la que lo quiere convertir: '); readln (base); end;
{Se determina el factor, el proceso obtendrá un valor por encima del deseado}
factor := 1; while factor<=num do factor:= factor*base;
{Imprimimos cada una de las cifras en orden}
while (factor div base) <> 0 do begin factor:=factor div base; cifra := num div factor; if cifra <10 then write(cifra) else write(chr(cifra-10+ord('A'))); num := num mod factor end; 101
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
writeln; end. Bucle con salida controlada En este caso, a diferencia de la instrucción WHILE…, el bucle con salida controlada se implementa empleando una sintaxis muy diferente entre los lenguajes de programación, así por ejemplo en Pascal se emplea la orden REPEAT…UNTIL…, en C, C++ y Java se emplea la orden do…while…, en Basic la orden se da con la sentencia DO…LOOP. En este caso la palabra reservada REPEAT (o DO en el caso de los otros lenguajes) sólo define el inicio del bloque que se va a iterar, aquí no vemos restricción alguna para ingresar y ejecutar el bloque de instrucciones. Es por esto que, a diferencia del WHILE, el bloque siempre se ejecutará por lo menos una vez. Luego de ejecutar el bloque de instrucciones se evalúa una condición, en el caso de la orden REPEAT esta condición está precedida por la cláusula UNTIL que indica que la iteración se repetirá hasta que la condición dé cómo resultado un valor verdadero. En el caso del C, Java, etc. la iteración se repetirá mientras la condición de cómo resultado un valor verdadero, en el caso del Basic se pueden emplear cualquiera de las dos alternativas. A continuación mostramos la sintaxis de la instrucción REPEAT…UNTIL…dada en al lenguaje Pascal: repeat
Bloque de instrucciones
until
Expresión lógica
Un ejemplo de cómo emplear la instrucción REPEAT…UNTIL… se muestra a continuación: {cálculo de los diez primeros valores enteros Compara con la versión hecha con while} entero := 0; suma := 0; repeat inc(entero); suma := suma + entero; until entero >= 10; Observe que allí, la inicialización de las variables son necesarias para que el proceso de cálculo se dé correctamente, sin embargo si se diera un valor inicial de 10 a la variable entero, las órdenes inc(entero); y suma := suma + entero; se ejecutarán, a diferencia de WHILE, en donde no se ingresaría al bloque de iteración.
Ejemplos de programas que emplean la instrucción repeat: 1. Como sabemos de los capítulos anteriores, Pascal no tiene un operador que permita elevar un número a una potencia dada u obtener la raíz n_ésima de un número. Este ejemplo permitirá calcular la raíz n_ésima de un número ( n Q en donde n será entero) empleando un proceso iterativo, en particular emplearemos una variante del método de Newton para determinar raíces de una ecuación. La raíz n_ésima de un número puede ser determinada empleando la siguiente expresión: 102
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
X n =
1 n
Q (n − 1)X 0 + n −1 X 0
donde n ∈ Ε
El proceso consiste en dar un valor cualquiera a X0 y determinar mediante la expresión el valor de Xn. Si X0 es igual a Xn se habrá obtenido la raíz, de lo contrario se reemplaza el valor de X 0 por el valor obtenido para X n y se vuelve a calcular la expresión. El proceso se repite hasta llegar a la convergencia. Datos: Se ingresará como dato el número del que se quiere obtener la raíz (Q) y el valor de la raíz (n). Resultados: Se deberá obtenerla el valor de n Q . Solución: La explicación de la solución prácticamente se detalló en el planteamiento del problema, por lo que en esta parte se analizarán algunos de los problemas que se pueden presentar en la implementación del problema. Lo primero que hay que tomar en cuenta es que la iteración debe terminar cuando X 0 se igual a Xn, sin embargo esto no puede darse en un programa debido a que ambas variables tendrán que ser de un tipo de dato numérico de punto flotante y, como vimos en los primeros capítulos, la representación de un número de punto flotante en la memoria del computador no dará un valor exacto. Esto quiere decir que si empleamos una expresión para calcular un número y el resultado debería dar por ejemplo 3, en la memoria del computador se puede estar almacenando el valor 2.999999, pero si por otro lado volvemos a evaluar la misma expresión empleando otros valores, pero la respuesta debiera volver a dar 3, es posible que se almacene en la memoria el valor de 3.000001. Para una persona común 2.999999 es prácticamente igual a 3.00001 sin embargo para el computador no lo es, es suficiente que un bit sea diferente para que la respuesta sea un valor falso. Es por esta razón que si colocamos en un programa un expresión como until X0 = Xn; es probable que el programa entre en una iteración infinita. Para solucionar este problema debemos establecer cuán precisa debe ser nuestra respuesta, esto es si queremos una precisión de 3, 4, 5 etc. dígitos decimales, una vez determinado esto, calculamos el error máximo permitido (para 3 dígitos decimales el error será 0.001, para 4 será 0.0001, etc.), luego restamos los dos valores (X0 - Xn) y si la diferencia diera un valor menos al error permitido se habrá encontrado la raíz buscada. En segundo lugar, vamos a tener que evaluar un exponente (X 0n-1), debido a que el exponente es un número entero, lo calcularemos por multiplicaciones sucesivas. Finalmente se presenta el problema de la inicialización de las variables. En el planteamiento del problema se dice que primero debemos dar un valor a X 0, con ese valor calculamos Xn, si seguimos rigurosamente esto vamos a encontrarnos con un problema al emplear una iteración con salida controlada, veamos esto en el siguiente pseudo código: X0 1 (el enunciado dice cualquier valor) Repetir Xn f(X0) (f es la expresión para calcular Xn en función de X0)
103
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
… aquí tendríamos que colocar una condicional que diga que si X 0 es diferente de Xn, reemplazar el valor de X0 por el valor de Xn(*). Hasta que X0 sea similar a Xn.
Allí podemos apreciar una incongruencia, si reemplazamos en valor de X 0 por el valor de Xn (*) dará como resultado que ambas variables tendrán el mismo valor, por lo tantos se habrá llegado a la condición de salida de ciclo iterativo, esto se producirá en la primera iteración sin haberse hallado la raíz. Por eso que, para solucionar el problema, se sugiere cambiar ligeramente el proceso. A continuación presentamos es pseudo código sugerido (en rojo se indicarán los cambios): Xn 1 (el enunciado dice cualquier valor) Repetir X0 Xn Xn f(X0) (f es la expresión para calcular Xn en función de X0) Hasta que X0 sea similar a Xn.
En el proceso se aprecia que en lugar de inicializar X 0, se inicializa Xn. Luego, la primera instrucción dentro del ciclo iterativo es reemplazar en valor de X 0 por el valor de Xn para luego calcular el nuevo valor de X n. En ese punto, en las primeras iteraciones los valores de X 0 y Xn serán sustancialmente diferentes a la hora de evaluar la salida del bucle, lo que resultará en una nueva iteración. Finalmente el proceso concluirá satisfactoriamente. Programa: El programa resultante se muestra a continuación: program deteminacionDelaRaiz_n_esima; const PRECISION=0.00001; var n, n1: Integer; q, x0, xn, exp: Real; begin write ('Ingrese los valores de n y Q: '); readln(n, q); Xn := 1; repeat x0 := xn; {Aquí calculamos el exponente} exp := 1; n1 := 0; repeat exp := exp*x0; inc(n1); until n1 = (n-1); {Aquí ya podemos calcular X }n xn := ( (n-1)*x0 + q/exp)/n; until abs(x0-xn) < PRECISION; writeln('El resultado es : ',x0:10:4); end. 104
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Variante: Se puede agregar al programa la verificación que los valores ingresados sean positivos, para esto el ingreso de datos podría modificarse como se indica a continuación: repeat write ('Ingrese los valores de n y Q: '); readln(n, q); until (n>0) and (q>0); Observe que no hay que dar valores iniciales a las variables.
Otras Estructuras Los diferentes lenguajes de programación definen otras estructuras iterativas y selectivas con la finalidad de hacer más fácil la codificación de un programa, sin embargo si es cierto que el concepto de estas estructuras es similar, la implementación que se hace en cada lenguaje de programación es muy diferente. Las estructuras que se estudiarán en este punto son la iteración con contador implícito y la selección múltiple. Bucle con contador implícito La idea de esta estructura es la de tener una variable a la que se le pueda dar un valor inicial y que se incremente automáticamente en cada ciclo, cuando su valor llegue a un tope preestablecido el ciclo iterativo concluye. En Pascal se emplea la instrucción FOR…TO/DOWNTO…DO, en C/C++, Java y C# se emplea un instrucción parecida denominada for(…,…,…), Basic utiliza la instrucción For…To…Step…. Todas con diferencias sustanciales. La sintaxis de este bucle en lenguaje Pascal se muestra a continuación: to for
Inicialización del índice
Límite downto
do
Bloque de instrucciones
Esta instrucción trabaja con una única variable denominada índice, esta variable debe ser de tipo ordinal (Integer, Char, Boolean, etc.), no permite variables de tipo real ni cadenas de caracteres. Al final de cada ciclo se incrementa en una unidad el valor de índice (si se emplea la cláusula TO) o se decrementa en una unidad (de usar DOWNTO). Cuando la variable sobrepasa el valor del límite da por concluida la iteración. En los lenguajes C, C++, Java y C# el funcionamiento es diferente, se puede emplear más de una variable índice, y estas pueden ser de cualquier tipo. Otra diferencia es que la instrucción no incrementa ni decrementa automáticamente sino que al final de cada ciclo ejecuta la sentencia que se coloca expresamente en la instrucción for, y ésta no tiene porqué ser un incremento o decremento. El For de Basic, permite el uso de cualquier tipo de dato numérico para la variable índice, que debe ser única, pero si se ejecuta un incremento o decremento obligatorio al final de cada ciclo, dependiendo del valor colocado en la cláusula STEP. 105
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Algunos ejemplos del uso de de esta instrucción se presentan a continuación: 1) for x:= 1 to 10 do writeln(x, ’^2 = ‘, sqr(x)); Este ejemplo muestra los valores del 1 al 10 acompañados sus respectivos cuadrados, el resultado será similar al cuadro siguiente: 1^2 = 1 2^2 = 4 3^2 = 9 4^2 = 16 5^2 = 25 6^2 = 36 7^2 = 79 8^2 = 64 9^2 = 81 10^2 = 100
2)
for x:= 10 downto 1 do writeln(x, ’^2 = ‘, sqr(x));
Este segundo ejemplo es similar al anterior pero la impresión se hace al revés. 10^2 = 100 9^2 = 81 8^2 = 64 7^2 = 79 6^2 = 36 5^2 = 25 4^2 = 16 3^2 = 9 2^2 = 4 1^2 = 1
3)
for letra:= ‘A’ to ‘Z’ do writeln(‘ASCII de ‘, letra, ’ = ‘, ord(letra));
Aquí el índice es una variable de tipo char y se pretende imprimir las letras mayúsculas acompañadas de su código ASCII. ASCII ASCII ASCII ASCII … ASCII ASCII ASCII
de de de de
A = 65 B = 66 C = 67 D = 68
de X = 88 de Y = 89 de Z = 90
Selección múltiple Cuando se emplea la instrucción IF sólo se pueden seguir dos caminos diferentes, con la selección múltiple se pretende tener la posibilidad de seguir varios caminos. Un esquema en diagrama de flujo se muestra a continuación: 106
Selección múltiple
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
?
A igual que con la instrucción FOR, la implementación de esta estructura es diferente en los lenguajes de programación, el lenguaje Pascal emplea la cláusula CASE…OF, C, C++, Java y C# emplean la orden switch, mientras que en Basic se emplea la instrucción SELECT CASE. La sintaxis para la orden CASE…OF se muestra a continuación: case
Expresión ordinal
of
Lista o rango de datos
:
Bloque de instrucciones
end else
Bloque de instrucciones
Esta instrucción empieza con la evaluación de una expresión de tipo ordinal, el resultado de esta evaluación va a ser buscado en las diferentes listas o rangos de datos. Si se encuentra, se ejecuta el bloque ligado a ella y se da por terminada la selección. Si no se encuentra y existe la cláusula else entonces se ejecuta ese bloque, de lo contrario se ignora la sentencia case y se continua con la ejecución del programa. A continuación se muestran algunos ejemplos del manejo de la instrucción CASE…OF: 1) El siguiente programa lee las notas de un alumno y le da una calificación cualitativa. program notaCualitativa; var p1, p2, p3, ex1, ex2: Integer; begin write('Ingrese Notas del Alumno : '); readln(p1, p2, p3, ex1, ex2); case round(((P1+P2+P3)/3+2*Ex1+3*Ex2)/6) of 0, 1, 2: writeln('Pésimo'); 3, 4, 5, 6: writeln('Muy Mal'); 7, 8, 9, 10: writeln('Mal'); 11, 12: writeln('Regular'); 13, 14, 15: writeln('Bien'); 16, 17, 18: writeln('Muy Bien'); 19, 20: writeln('Excelente'); else writeln('Error en Datos'); end end. 107
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
En este ejemplo se ingresa como dato las notas de 3 prácticas y dos exámenes, luego el programa ingresa a la instrucción SELECT CASE, se evalúa el promedio del alumno, para luego, buscar el resultado en las listas de la orden, al encontrarlo se imprime un mensaje con el calificativo cualitativo. Así si se ingresa como dato los valores 12 16 19 18 17 se imprimirá el mensaje “Muy Bien” ya que al calcular el promedio se obtiene el valor 17. 2) El siguiente programa simula el uso de una calculadora elemental que sume, reste, multiplique y divida. program calculadoraElemental; var num1, num2: Real; operador: char; begin write('Dato : '); readln(num1); repeat write('Operación : '); readln(operador); if operador <> '=' then begin write('Dato : '); readln(num2); end; case operador of '+': num1 := num1 + num2; '-': num1 := num1 - num2; '*','x', 'X’: num1 := num1 * num2; '/',’÷’: num1 := num1 / num2; end; writeln('-----------------------'); writeln(num1:21:3); until operador = '=' end. Al ejecutar el programa usted observará algo similar a lo siguiente: Dato : 34.5 Operación : * Dato : 2.41 ----------------------83.145 Operación : / Dato : 3.9 ----------------------21.319 Operación : + Dato : 12 ----------------------33.319 Operación : = ----------------------33.319
108
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Solución de problemas con estructuras de control Técnicas para la resolución de programas que requieren el ingreso de una gran cantidad de datos: La idea en esta sección es la de poder apreciar que un programa que resuelve un problema puede desarrollarse de muchas maneras diferentes, sin embargo, a pesar que todas darán un resultado correcto, no todas las soluciones serán igualmente eficientes ni serán igualmente aceptadas. Se planteará un problema sencillo, con la finalidad de analizar los detalles de la eficiencia del programa. El problema que se plantea es el de calcular un promedio que se obtendrá a partir de una gran cantidad de datos que se ingrese al programa. Alternativa A: Inicialmente se ingresa el número de datos que se va a procesar y a continuación los datos para hallar el promedio: Program promedioDeDatosVs01; var dato, numDat, suma, d: integer; prom: real; begin write(‘Ingrese el número de datos: ‘); readln(numDat); suma := 0; for d:= 1 to numDat do begin readln(dato); suma:= suma + dato; end; prom:=suma/numDat; writeln(‘Promedio = ‘, prom:8:1); end. Con la premisa del ingreso del número de datos, el programa se torna muy sencillo, la respuesta es casi inmediata. Sin embargo, pongámonos a pensar en el usuario que ejecutará el programa. Esa persona tiene el trabajo de digitar cada uno de los datos que se ingresará al programa, y según el planteamiento del problema son muchos. La manera en la que se ha diseñado el programa hace que además de los datos, el usuario tenga la tarea de contar cuántos datos tiene. Las computadoras se han diseñado para aliviar los problemas, no para incrementarlos. Póngase a pensar que pasaría si el usuario se equivoca al contar, luego de ingresar todos los datos el programa le sigue pidiendo más datos, o luego de ingresar un gran número de datos el programa ya no le deja ingresar más datos y peor aun da una respuesta antes de concluir con el ingreso de todos los datos. Esto no se puede concebir, el usuario no debe procesar manualmente datos para que el programa trabaje correctamente. Alternativa B: El programa irá pidiendo un dato a la vez, el usuario deberá confirmar si se ingresa o no un dato más cada vez: 109
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
program promedioDeDatosVs02; var dato, numDat, suma : Integer; prom : Real; opcion : Char; begin numDat:=0; suma :=0; writeln(‘Ingrese los datos:’); repeat readln(dato); suma := suma + dato; write(‘¿Desea ingresar un nuevo dato (s/n)?’ ); readln(opcion); until opcion = ‘N’; prom:=suma/numDat; writeln(‘Promedio = ‘, prom:8:1); end. Esta solución, como puede apreciar, es más elaborada que la primera, aquí se puede ver que el usuario ya no contará los datos sino que es el programa el que lo hará. A pesar de esto no es una buena solución, sólo hay que darse cuenta cómo será la ejecución del programa en cuanto al ingreso de los datos. Primero sale el mensaje Ingrese los datos:, luego el usuario digita su primer dato y presiona enter, inmediatamente le aparece el mensaje ¿Desea ingresar un nuevo dato (s/n)? con lo que se verá obligado escribir la letra S seguido del enter. El proceso se repite hasta que el usuario ingrese una letra N (mayúscula). Observe que el usuario tendrá que ingresar el doble de información (el dato y la letra) que la que debe ingresar; más aun qué pasará si cuando ingresó todos los datos ingresa por error una n (minúscula), pues tendrá que volver a empezar. Alternativa C: El programa controlará el ingreso de información mediante una condición de fin de datos. La idea en esta alternativa es la de ingresar uno a uno los datos y cuando se terminen se ingresa un valor adicional con un valor que no se encuentre en el rango en que se encuentren los datos. Por ejemplo si los datos son las notas de los alumnos en un curso, luego de ingresar las notas se ingresa un valor como 21, de modo que el programa se dé cuenta que ya se terminaron los datos, si los datos son positivos se puede ingresar un valor como -1 al final. La limitación en esta solución es que hay que estudiar los datos que se ingresarán al programa para definir una buena condición de salida. A continuación se muestra este programa: program promedioDeDatosVs03; var dato, numDat, suma: Integer; prom : Real; 110
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
begin writeln('Ingrese los datos, para terminar ingrese un número negativo: '); numDat:=0; suma:=0; repeat read(dato); if dato >=0 then begin suma := suma + dato; inc(numDat); end until dato < 0; prom:= suma/numDat; writeln('El promedio es : ',prom:8:2); end. Se puede ver que es un programa más elaborado que los anteriores, aunque no es perfecto, es una buena alternativa. Un defecto que tiene esta solución es que existe la posibilidad que el usuario se equivoque al ingresar un dato. Luego de ingresarlo ya no podrá corregirlo, lo que hará que se tenga que ejecutar el programa nuevamente, por otro lado si luego de un tiempo quisiera volver a ejecutar el programa con los mismos datos tendría que ingresarlos uno a uno nuevamente. La solución a este problema es el uso de archivos de texto; esta alternativa se presentará más adelante luego de detallar la forma en que se manejan los archivos de texto. Programas que emplean archivos de texto y estructuras de control En el capítulo anterior ya se estudió cómo se puede acceder a un archivo de texto, en esta sección vamos a ampliar los conocimientos acerca de estos archivos y vamos a mostrar diferentes maneras de manipular estos archivos. Los archivos de texto son una secuencia de caracteres almacenado en el computador. Esta secuencia de caracteres está dividida o agrupada en “líneas” o “registros”, cada línea puede tener un número diferente de caracteres y la cantidad de caracteres que puede tener una línea no tiene límite. Por esta razón, para poder determinar dónde empieza y dónde termina una línea en un archivo, entre línea y línea se colocan dos caracteres especiales que marcan esta delimitación. Estos caracteres especiales, como lo indicamos en el capítulo anterior corresponden al “carriage return” (cuyo código ASCII es 13) y al “line feed” (con código ASCII 10). Un procesador de palabras al cargar un archivo de textos mostrará cada uno de los caracteres del archivo, cuando encuentra estos caracteres especiales, los siguientes caracteres los muestra en la línea siguiente, de allí el nombre de “líneas” que se da para los registros de este tipo de archivos. Esta secuencia de caracteres y líneas que contiene un archivo de textos tampoco tiene límites, por eso se requiere poner otra marca en el archivo de modo tal que se pueda 111
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
saber dónde termina este archivo. Esta marca se establece con el caracter cuyo código ASCII en el 26 (end of file). A continuación se muestra un programa que permite apreciar estas características. Pero antes habrá que crear un archivo de textos, para esto abra el “bloc de notas” del Windows y escriba lo siguiente:
Luego guárdelo con el nombre de archtext.txt en la misma carpeta donde escribirá el siguiente programa. program leeArchivoCaracterPorCaracter; {Detecta los cambios de línea y el fin del archivo} var c: Char; arch: Text; begin assign(arch, 'archtext.txt'); reset(arch); while true do begin {no se pone condición de salida aquí} read(arch,c); {Si el caracter es un “caracter imprimible” (su código ASCII es mayor o igual a 32) se muestra como tal, de lo contrario se muestra su código ASCII precedido por el caracter #} if ord(c) >= 32 then write(c) else write('#',ord(c)); if ord(c) = 26 then break; {condición de salida} end end. Luego de ejecutar el programa podrá ver como resultado algo similar a lo que se muestra en la figura siguiente, observe la presencia de los caracteres que marcan el cambio de línea y el fin de archivo.
Los caracteres de cambio de línea y fin de archivo pueden ser detectados fácilmente cuando en el programa se lee el archivo caracter por caracter, sin embargo no se puede detectar si lo que se lee se maneja con otro tipo de variables (entera, reales, cadenas de caracteres, etc.), simplemente el programa se las saltará, pudiendo entrar en iteraciones indefinidas en el caso que se encuentre al final del archivo. Para 112
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
poder hacerlo, se debe hacer uso de funciones diseñadas para este fin en los diferentes lenguajes de programación, en Pascal estas funciones son eof y eoln. Las funciones eof y eoln tienen como parámetro la variable de archivo conectada con el archivo que se está leyendo, esto es eof(arch) y eoln(arch), y devuelven un valor lógico (boolean) que será verdadero (true) si el indicador del archivo se encuentra en el fin de archivo, para el caso de eof, o en el fin de una línea, para eoln. Para otro caso se devuelve el valor falso (false). Con estas funciones se podrá escribir algoritmos que permitan realizar las siguientes tareas …mientras no sea el fin del archivo… leer datos del archivo y procesarlos… o …mientras no sea el fin de la línea… leer datos del archivo y procesarlos… Alternativa D: Aquí presentamos la última alternativa al problema planteado en la sección anterior, de lo que se trata esta vez es que el usuario copie sus datos en un archivo de textos, el programa leerá del archivo los datos e imprimirá el resultado, de esta forma el usuario podrá ejecutar el programa con los mismos datos todas las veces que quiera sin tener que digitarlos nuevamente, y si se diera cuenta que hay errores en los datos, sólo tendría que abrir el archivo con los datos, corregir los datos defectuosos, grabarlo nuevamente y ejecutar el programa. program promedioDeDatosVs04; Var dato, numDat, suma: Integer; prom: Real; arch: Text; nombArch: String; begin writeln('Ingrese el nombre del archivo de datos'); readln(nombArch); assign(arch, nombArch); reset(arch); numDat := 0; suma := 0; while not eof(arch) do begin read(arch, dato); suma := suma + dato; inc(numDat); end; prom:= suma/numDat; writeln('El promedio es : ',prom:8:2); end. Observe que el programa pide al usuario el nombre del archivo, esto para permitirle al usuario mantener varios archivos con diferentes datos. 113
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Ejercicios diversos 1. Escriba un programa que permita contar la cantidad de valores que hay en cada línea de un archivo similar al que se muestra a continuación: 12.34 129.345 23 13 12 18 22 45.098 78.45 100 205.345 …
87.01
102.3
Observe que el cada dato puede estar separado por más de un espacio en blanco o tabulador. program centaDeDatosPorLinea; var numDat, linea: Integer; dato: Real; arch: Text; nombArch: String; begin write('Ingrese el nombre del archivo de datos: '); readln(nombArch); assign(arch, nombArch); reset(arch); linea:=1; while not eof(arch) do begin numDat:=0; while not eoln(arch) do begin read(arch, dato); inc(numDat); end; readln(arch); writeln('Linea ', linea, ' : ', numDat); inc(linea); end; end. Al ejecutar este programa, con los datos que presenta en el planteamiento del problema, obtendremos el siguiente resultado:
2. Escriba un programa que permita determinar el promedio de ventas que ha realizado cada uno de los vendedores de una compañía, los datos se encuentran en un archivo de texto con el siguiente formato: 114
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Paula Gómez Roca 1309.34 1200.50 180 13 1450.0 Juan García Castro 456.09 102.24 333.33 Ana Rodríguez Narváez 7801.45 1000.0 2075.34 12345.67 …
En la primera línea del archivo se encuentra en nombre de un vendedor, en la segunda línea se encuentra la lista de los montos facturados por ese vendedor. En la línea siguiente aparece otro vendedor y en la siguiente lo que facturó, y así sucesivamente. Solución: La solución estará encaminada a generar un ciclo iterativo, en cada uno de los ciclos debe procesarse un vendedor, las iteraciones se terminan cuando se llegue al final del archivo. El procesamiento de un vendedor se debe realizar leyendo primero el nombre del vendedor del archivo de textos, luego se leen una a una las cantidades numéricas y se acumulan, llevando la cuenta del número de valores, este proceso se realiza en una nueva iteración que concluye con el cambio de línea. Al salir de esta iteración se calcula el promedio y se imprime el nombre del vendedor y el promedio. Programa: El programa resultante se muestra a continuación: program promedioPorVendedor; var numDat: integer; venta, suma, prom: real; arch: text; nombArch, vendedor: String; begin write('Ingrese el nombre del archivo de datos: '); readln(nombArch); assign(arch, nombArch); reset(arch); while not eof(arch) do begin readln(arch, vendedor); numDat:=0; suma := 0; while not eoln(arch) do begin read(arch, venta); suma := suma + venta; inc(numDat); end; readln(arch); writeln(vendedor:35, ' : ', suma/numDat:10:2); end; end. Este programa, luego de ejecutarse mostrará un resultado similar al siguiente:
115
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
3. Escriba un programa que permita contar la cantidad de caracteres “no blancos” en un archivo de textos. Los caracteres “no bancos” corresponde a los espacios en blanco, cambios de línea y tabulaciones. Solución: La idea para solucionar este problema es ir leyendo uno a uno los caracteres de un archivo y se irán contado uno a uno, se descartarán todos aquellos que se consideren “no blancos”. program cuentaCaracteresNoBlancos; var cont: integer; arch: text; nombArch: String; c: char; begin write('Ingrese el nombre del archivo de datos: '); readln(nombArch); assign(arch,nombArch); reset(arch); cont:=0; while not eof(arch) do begin read(arch,c); if not (c in [' ', #9, #13, #10]) then inc(cont); { ‘ ‘ -> espacio en blanco, #9 -> código ASCII del caracter de tabulación, #13 -> código ASCII del caracter de retorno de carro, #10 -> código ASCII del caracter de cambio de línea } end; writeln('Número de caracteres no blancos: ',cont); end. Al ejecutar el programa, empleando un archivo como vemos a continuación:
Obtendremos el siguiente resultado:
4. Escriba un programa que permita contar el número de palabras que se encuentran en un archivo de texto. Las palabras pueden estar separadas por uno o más espacios en blanco, tabuladores o cambios de línea. Solución: el programa deberá leer caracter por caracter el archivo, cuando detecte el inicio de una palabra la contará. El inicio de una palabra se detecta cuando el caracter que se ha leído es un caracter “no blanco” y el caracter leído inmediatamente antes es un caracter “blanco”. 116
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
program cuentaPalabras; var cont: integer; arch: text; nombArch: String; c, cAnt: char; begin write('Ingrese el nombre del archivo de datos: '); readln(nombArch); assign(arch,nombArch); reset(arch); cont := 0; cAnt := ' '; {se inicializa con un espacio para poder contra la primera palabra} while not eof(arch) do begin read(arch,c); if (cAnt in [' ', #9, #13, #10]) and not (c in [' ', #9, #13, #10]) then inc(cont); cAnt:=c; end; writeln('Número de palabras: ', cont); end. Si empleando un archivo como el que vemos a continuación para ejecutar el programa:
La salida del programa será como sigue:
Escriba un programa que permita modificar el contenido de un archivo de textos. El archivo será similar al siguiente: En el presente mes se vendieron aproximadamente 250 artículos del tipo A y 300 del tipo B obteniéndose ingresos de $4768 y $ 7251 respectivamente con lo que la ganancia acumulada hasta el momento es de $25547, esto es, alrededor de 57% más que el año pasado.
El programa ingresará el nombre de un archivo con textos y el nombre del archivo de salida, además un porcentaje de incremento. El programa deberá un archivo de salida con el mismo texto pero modificando todos los valores numéricos que encuentre, incrementándolos en el porcentaje dado. 117
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Para el ejemplo si se ingresa 17%, se obtendrá: En el presente mes se vendieron aproximadamente 292 artículos del tipo A y 351 del tipo B obteniéndose ingresos de $5578 y $ 8483 respectivamente con lo que la ganancia acumulada hasta el momento es de $29889, esto es, alrededor de 66% más que el año pasado.
Sólo se trabajará con valores enteros. Solución: Como en el archivo de textos, los números no están colocados de acuerdo a un patrón determinado, el programa deberá leer el archivo caracter por caracter. Conforme lee un caracter lo imprimirá inmediatamente en el archivo de salida, pero si en la lectura de un caracter se detecta el inicio de un número, este caracter junto con los que le siguen se tomarán y se transformarán en una representación numérica. Una vez obtenido el número se incrementará en el porcentaje dado y se escribirá en el archivo de salida. program modificaNumerosEnUnTexto; var nombArchIn, nombArchOut: String; archIn, archOut: Text; porcent, numero: Integer; car: Char; begin write('Ingrese el nombre del archivo de entrada: '); readln(nombArchIn); assign(archIn,nombArchIn); reset(archIn); write('Ingrese el nombnre del archivo d esalida: '); readln(nombArchOut); assign(archOut,nombArchOut); rewrite(archOut); write('Ingrese el porcentaje: '); readln(porcent); numero:=0; while not eof(archIn) do begin read(archIn,car); while car in ['0'..'9'] do begin {Si caracter es un dígito, procesamos el número} numero := numero * 10; numero := numero + ord(car)-ord('0'); {leemos un nuevo caracter} read(archIn, car); if not (car in ['0'..'9'] ) then begin { Si caracter no es un dígito indica que se terminó de procesar el número, entonces lo incrementamos por el porcentaje y lo imprimimos} numero := trunc(numero*(1+porcent/100)); 118
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
write(archOut,numero); numero:=0;
end end; write(archOut,car);
end; close(archOut); end.
119
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 6: Programación modular: Como vimos en el capítulo 3, cuando empezamos a analizar un problema muchas veces tendremos la sensación que el problema es muy complicado, que es tan grande que nos va a aplastar. Problema
También vimos que una manera de resolver el problema es empezar por dividir el problema en partes, tratando de no entrar en los detalles de la solución. En ese capítulo se menciona el problema de cómo generar un listado ordenado descendentemente por notas, de un determinado curso. Se vio allí que el problema se podía dividir en las siguientes tareas: Tareas: 1- Obtener los datos 2- Separar a los alumnos que llevan el curso 3- Obtener los promedios de cada alumno 4- Ordenar a los alumnos por promedio 5- Imprimir datos
Observe que las tareas que se han llegado a establecer, además de darnos una idea general de lo que tenemos que hacer para solucionar el problema, tienen algunas características. En primer lugar cada tarea es independiente de la otra, esto quiere decir que si tomamos una de las tareas podemos tratar de resolverla independientemente si se tiene o no una solución para las otras. Por ejemplo la tarea 2, “Separar los alumnos que llevan el curso”, teniendo la lista de alumnos podemos separarlos independientemente de saber cómo se obtuvo la lista (por el teclado, por un archivo de textos, etc.), tampoco interesa aquí qué se va a hacer con la lista seleccionada (se va a ordenar por nombres, por promedio ponderado o por un curso, o si se va o no a imprimir y en qué medio). Esta característica hace que, como habrá podido darse cuenta, podamos empezar a solucionar el problema por cualquiera de sus tareas, sin necesidad que sea en un orden determinado, y más aun podemos solucionar una parte del problema sin tener idea de cómo se va a solucionar alguna otra tarea. Otra característica de esto es que las tareas que se describen no están detalladas, no se menciona por ejemplo el nombre del archivo de donde se obtendrán los datos, qué método se va a emplear para ordenar los datos, ni la forma cómo se va a obtener la nota del curso. Esos detalles se irán colocando conforme ataquemos cada uno de los problemas. Una vez descritas las tareas que tiene el problema, se coge una de las tareas y se le empieza a analizar. Es muy probable que esa tarea sea aun muy compleja, pues bien, a esa tarea le podemos aplicar el método anterior, esto es subdividirla en una serie de subtareas, por ejemplo: 120
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Tarea: Obtener los promedios de cada alumno 1- Tomar un alumno 2- Calcular el promedio de prácticas 3- Calcular el promedio de laboratorios 4- Aplicar la fórmula (2xPPr + PLab + 3xEx1 + 4Ex2)/10 para obtener la nota del curso 5- Si hay más alumnos, repetir los pasos 1 al 5
Aquí podrá observar que dentro de la lista de tareas hay alguna que es muy trivial y que con sólo una línea de código en algún lenguaje de programación estará solucionado, otras no y tendrán que aplicárseles nuevamente el método, esto hasta que todas las tareas sean muy simples de solucionar. Esta metodología de solución de problemas ha sido utilizada por muchos años con gran éxito y se denomina “Diseño descendente”, se espera, en este texto, que usted pueda llegar a dominar esta metodología. Otra característica de esta metodología es que una vez definida la lista de tareas de un problema se puede conformar varios equipos de personas, a cada equipo se le puede entregar una tarea que deberá resolver. Esto hará que la solución del problema se dé más rápidamente, porque estarán trabajando en paralelo. Cada grupo dará solución a una tarea, que entregará en lo que se denomina un “módulo” con todas las subtareas resueltas, luego se juntarán los módulos y se tendrá la solución para el problema inicial, esto se conoce con el nombre de “programación modular”. Otra ventaja que se puede apreciar en esta forma de trabajar es que si luego de implementar la solución nos damos cuenta que una parte del programa no es muy eficiente, por ejemplo se demora mucho en mostrar los resultados, podemos reemplazar el módulo por otro que resuelva la misma tarea, pero más eficiente, sin tener que modificar todo el programa.
Implementación de la programación modular Los diferentes lenguajes de programación modernos permiten implementar programas haciendo uso de la técnica de programación modular. Las unidades básicas que nos brindan los lenguajes de programación para este fin se denominan “funciones” y “procedimientos”, estas unidades básicas también reciben el nombre de “subprogramas”. Una función se puede definir como un conjunto de instrucciones que tiene como finalidad obtener un valor resultante. Si nosotros vemos dentro la biblioteca de funciones del Pascal, podemos observar que por ejemplo que en la instrucción b := cos(0.5236); Se encuentra una función, cos. Aquí vemos claramente porqué se le denomina función, a la función se le entrega un valor (0.5236) y con él se realizan internamente una serie de operaciones que conducen a determinar el valor del coseno de ese número. Una función sólo puede devolver un valor. Un procedimiento también se puede definir como un conjunto de instrucciones, pero a diferencia de las funciones, los procedimientos no buscan obtener un valor resultante, 121
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
sino que su finalidad es la de realizar un proceso. Veamos por ejemplo writeln, si lo colocamos en una instrucción como la siguiente: writeln( ‘X1 = ‘, parteReal:8:3,’ +’, parteImag:8:3,’i’); Se puede ver que aquí no se busca obtener un resultado, primero porque writeln no devuelve valores; lo que se pretende aquí realizar el proceso de mostrar en la pantalla del computador el contenido de las variables parteReal y parteImag acompañados de un texto de explicación y con un formato dado. Si se concibe un proceso en el que se deba devolver más de un resultado se debe pensar más en un procedimiento que en una función, por ejemplo si se quisiera realice un proceso para determinar las coordenadas X, Y de un punto determinado. Se debe aclarar que en el lenguaje Pascal las funciones y los procedimientos se declaran de una manera diferente, sin embargo en otros lenguajes como el C, C++, Java, etc. se emplea la misma sintaxis para declarar ambos, es por eso que en estos lenguajes se les denomina indistintamente funciones a ambos procesos. Sintaxis de una función en Pascal: function
Identificador con el nombre de la función (
: Lista de parámetros
Tipo de resultado
;
Bloque de instrucciones
Declaración de identificadores locales
)
Sintaxis de un procedimiento en Pascal: rocedure
Identificador con el nombre del procedimiento
Bloque de instrucciones
; (
Lista de parámetros
)
Declaración de identificadores locales
A continuación se mostrarán ejemplos de cómo implementar estos elementos en un programa. 1. Escriba un programa que permita imprimir cinco valores aleatorios entre 5 y 15. Solución: La solución del problema es sencilla, sólo hay que calcular el valor aleatorio, imprimirlo y repetir estos pasos 5 veces. Para el cálculo del valor aleatorio, sabemos que en Pascal existe una función que me devuelva el valor aleatorio entre 0 y n-1, pero no existe una función que nos de un valor entre 5 y 15; si es cierto que el problema se puede plantear sin usar subprogramas (y de hecho ya lo hicimos en capítulos anteriores), esta vez vamos a implementar una función que permita calcular un valor aleatorio entre 5 y 15 de la siguiente manera: Primero implementaremos el programa principal, la idea aquí es suponer que el lenguaje Pascal tiene dentro de su biblioteca de funciones, una que se llame, por ejemplo, alatorio5_15 (de la misma manera existen random, sin, sqrt, etc.) y que nos da el 122
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
valor que necesitamos. Bajo este supuesto podemos escribir nuestro programa de la siguiente manera: program valoresAleatoriosEntre_5_y_15; var i, valor: Integer; begin randomize; for i := 1 to 5 do begin valor := alatorio5_15; writeln(i:2,') ', valor:2); end; end. Si la función alatorio5_15 existiera, nuestro trabajo habría terminado, pero como no es así, debemos implementar esa función, por lo que a continuación mostramos el programa completo program valoresAleatoriosEntre_5_y_15; function alatorio5_15: Integer; begin alatorio5_15 := random(15-5+1) + 5; end; var i, valor: Integer; begin randomize; for i := 1 to 5 do begin valor := alatorio5_15; writeln(i:2,') ', valor:2); end; end. Observe que en la implementación de la función, la manera de devolver el valor resultante de la función es asignarlo al identificador con el nombre de la función (en otros lenguajes de programación como C, C++, etc. la devolución del valor se hace a través de una instrucción denominada return). La salida del programa será como sigue:
2. El siguiente programa muestra el uso de un procedimiento en un programa. Solución: Lo que se busca en este ejemplo es que usted pueda observar que el programa principal, se va transformar en una lista de tareas, las que serán resueltas en el desarrollo de los procedimientos. Al igual que se hizo con la función del ejemplo anterior se debe escribir el programa pensando, en este caso, que los procedimientos existen, de modo que la solución planteada se base a la existencia de estos 123
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedimientos y sea por tanto muy simple. En ese sentido, podemos plantear un programa de la siguiente manera: program implementaciondeProcedimientos; begin randomize; imprimeEncabezados; muestraValoresAleatorios; imprimeFinal; end. Luego de dar la solución general, se procede a desarrollar el nivel en el diseño descendente, y así hasta acabar con el problema. A continuación se muestra el programa luego de haberse completado los niveles: program implementaciondeProcedimientos; procedure imprimeEncabezados; begin writeln('Este es un ejemplo del uso de'); writeln('procedimientos en un programa'); writeln; writeln('Resultados esperados:'); end; procedure muestraValoresAleatorios; begin writeln('1) ',random(10)+1); writeln('2) ',random(10)+1); writeln('3) ',random(10)+1); writeln('4) ',random(10)+1); writeln('5) ',random(10)+1); end; procedure imprimeFinal; begin writeln('Fin del programa'); writeln('Fecha: 26/08/2009'); end; begin randomize; imprimeEncabezados; muestraValoresAleatorios; imprimeFinal; end.
124
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Como se puede observar, las funciones y procedimientos son muy parecidos, sin embargo a diferencia de las funciones, los procedimientos, como no devuelven resultados, no requieren asignar un resultado al identificador que lleva su nombre. Al ejecutar el programa obtendremos el siguiente resultado:
Variables globales, locales y estáticas Variables Globales: Una variable global es aquella variable que ha sido declarada fuera del ámbito de los módulos de un programa, tienen como característica principal que, dependiendo de la ubicación de su declaración, pueden ser utilizadas dentro del código del programa principal y también dentro del código de las funciones y procedimientos del programa. Observe el siguiente programa: {01} program usoDeVariablesGlobales; {02} var vGlobal: Integer; {03} function f1: Integer; {04} begin {05} vGlobal := 17; {06} f1 := 3 * vGlobal; {07} end; {08} procedure p1; {09} begin {10} writeln('Dentro del procedimiento p1:'); {11} vGlobal := 2 * vGlobal; {12} writeln(' La variable vGobal vale ahora: ', vGlobal); Ámbito de la {13} writeln(' Fin del procedimiento p1'); writeln; variable vGlobal {14} end; {15} {Programa principal} {16} var a: Integer; {17} begin {18} vGlobal := 5; {19} writeln('Al inicio del programa principal, vGlobal = ',vGlobal); Ámbito de la variable a {20} a:= f1; writeln; {21} writeln('Luego de ejecutar la función f1, vGlobal = ',vGlobal); {22} writeln('A = ',a); writeln; {23} p1; {24} writeln('Luego de ejecutar el procedimiento p1, vGlobal = ',vGlobal); {25} end. 125
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
La variable vGlobal ha sido declarada al inicio del programa, antes de la declaración de funciones y procedimientos, esta variable podrá ser utilizada en cualquier parte del programa, incluso dentro de los subprogramas, es por eso que se denominan globales. Esta característica global de las variables tiene validez desde el punto en que son declaradas hacia delante en el programa, esto quiere decir que si la variable vGlobal hubiera sido declarada entre las líneas {07} y {08} se le podría utilizar en el procedimiento p1 y en el programa principal pero no en la función f1. A esta característica se le denomina ámbito de la variable. Esta propiedad se da por la forma cómo trabajan los compiladores, en el proceso de compilación las líneas de un programa se van traduciendo secuencialmente (desde la primer a la última), por eso, una línea en la que se encuentre la variable, no podrá ser traducida si es que antes el compilador no tradujo la orden en la que se define las propiedades de esa variable (declaración). En el programa anterior se puede apreciar el ámbito de la variable vGlobal así como el de la variable a. La ejecución del programa empieza en el programa principal, en la línea {18} se le asigna a la variable vGlobal el valor de 5, luego se llama a la función f1 en la línea {20}, trasladándose el control del programa a la línea {05}, allí se cambia el valor de vGlobal por 17, manteniéndose ese valor incluso luego de terminada la ejecución de la función. Cuando f1 termine, se retorna a la línea {20}. Luego al llegar a la línea {22} se llama al procedimiento p1, transfiriéndose el control a la línea {10}, en la línea siguiente se vuelve a cambiar el valor de vGlobal a 34, valor que se mantiene hasta terminar el programa. La ejecución del programa es como sigue.
Las variables globales, si es cierto que son muy prácticas para elaborar un programa, nos pueden dar más de un dolor de cabeza cuando trabajamos con muchos subprogramas y sobre todo cuando los subprogramas están en otros archivos y han sido elaborados por diferentes personas. Esto se da precisamente porque no se tiene el control de la variable. Por ejemplo analice esta porción de código: {01} var base, altura, area: Integer; {02} begin {03} base := 5; altura := 6 {04} imprimeEncabezados; {05} area := base * altura / 2 ; {06} writeln('El área del triángulo es = ', area:10:2); {07} end. Si las variables base, altura y area fueran declaradas como variables globales, nadie nos podría garantizar que el área que calculemos sea 15 (5*6/2). Esto simplemente porque entre la asignación de los valores y el cálculo del área se encuentra la invocación a un procedimiento llamado imprimeEncabezados. Este procedimiento, 126
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
además de imprimir el encabezado del reporte final, podría hacer uso de las variables globales y por lo tanto podría modificar los valores de esas variables por cualquier otro que se le ocurra, y esto no lo podríamos controlar o evitar porque el procedimiento pudo haber sido elaborado por otro grupo de personas y podría ser muy extenso, y esto traería como consecuencia que el área que queremos calcular no sería la esperada, produciendo un error grave en el resultado del programa y que no nos daríamos cuenta de ello hasta que sea demasiado tarde. Esto se debe evitar, la programación moderna busca independizar los módulos en un programa, encapsulando la información que manejan, de modo que lo que se haga en un módulo no afecta a otros. Es por esta razón que un programa no debe tener variables globales, en Pascal esto se puede lograr definiendo las variables del programa principal después de todos los módulos del programa, así el ámbito de la variable sólo afectará al programa principal y las variables no podrán ser manipuladas por los subprogramas. Las variables globales son inicializadas por lo general en cero (o con el caracter nulo o con una cadena nula en el caso de otros datos). Variables Locales: Las variables locales, son variables que se definen o declaran dentro de un subprograma, estas variables se crean en el instante en que se empieza a ejecutar el subprograma y se destruyen cuando éste termina. Si el subprograma es invocado varias veces en un programa, las variables locales declaradas en él, se crearán y destruirán tantas veces como el subprograma sea invocado. Esta característica hace que las variables definidas como locales no puedan ser utilizadas por otros subprogramas ni tampoco por el programa principal, incluso dos subprogramas podrían definir sus propias variables locales y emplear los mismos nombres sin que se afecten unas contra otras. El siguiente programa muestra el empleo de variables locales; allí se va a calcular el promedio y la desviación estándar de los valores contenidos en un archivo de textos (datosVL.txt). Primero se muestra el programa principal, que es lo que primero debe escribirse en un programa, para poder darse cuenta qué se pretende con el programa. program usoDeVariablesLocales; var prom, dE: Real; begin {Calculamos el promedio de los datos de un archivo} prom := promedio; {llamamos a la función promedio} dE := desvEst; {llamamos a la función desvEst} writeln('Calculos realizados'); writeln('Promedio : ', prom:10:2); writeln('Desviación Estándar : ', dE:10:2); end. A continuación se completa el programa agregándole la implementación de las funciones. Observe que dentro de cada función se han definido variables, las cuales sólo podrán ser utilizadas dentro de la función que la declaró, esa es la razón por la 127
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
que se ha tenido que declara por ejemplo la variable suma en las dos funciones ya que en ambas funciones se tienen que acumular una serie de valores. Esta doble declaración puede parecer un fastidio envés de una ayuda, sin embargo piense por un momento que ese programa puede ser parte de otro mucho más complejo, que por el tamaño del programa se ha tenido que dividir éste entre varias personas o equipos, y que las funciones promedio y desvEst serán elaboradas por grupos diferentes; pues en cada una de ellas debe tener la facultad definir las variables (y los nombres de ellas) que se necesite para resolver su problema sin tener que preocuparse por la variables ( o los nombres de ellas) que se usan en otros módulos. program usoDeVariablesLocales; function promedio: Real; {variables locales de la función promedio} var arch: Text; numDat: Integer; dato, suma: Real; begin suma := 0; numDat:= 0; assign(arch, 'datosVL.txt'); reset(arch); while not eof(arch) do begin read(arch, dato); inc(numDat); suma := suma + dato; end; close(arch); promedio := suma / numDat; end; function desvEst: Real; {variables locales de la funcion desvEst} var arch: Text; numDat: Integer; dato, suma, sumaCuad: Real; begin suma := 0; sumaCuad := 0; numDat:= 0; assign(arch, 'datosVL.txt'); reset(arch); while not eof(arch) do begin read(arch, dato); inc(numDat); suma := suma + dato; sumaCuad := sumaCuad + dato*dato; end; close(arch); desvEst := sqrt((numDat*sumaCuad - sqr(suma))/(numDat*(numDat-1))); end; var prom, dE: Real; begin {calculamos el promedio de los datos de un archivo} prom := promedio; dE := desvEst; 128
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
writeln('Calculos realizados'); writeln('Promedio : ', prom:10:2); writeln('Desviación Estándar : ', dE:10:2); end. Variables Estáticas: Las variables estáticas son variables que, como las variables locales, sólo se pueden emplear en el subprograma que la declaró, pero a diferencia de éstas últimas no se destruyen cuando. Cuando el subprograma es nuevamente invocado mantiene el valor que tenía cuando termino la ejecución del subprograma la vez anterior. La manera de declarar una variable estática es muy simple en la mayoría de lenguajes de programación, ya que sólo hay que anteponer la palabra “static” a la declaración de la variable; sin embargo en Pascal estas variables tienen una forma peculiar de definirse, la declaración se hace mediante la siguiente sintaxis: const
Identificador
:
Ti o de dato
=
Valor
;
,
El programa siguiente muestra el uso y el comportamiento de una variable estática, allí puede comparar estas variables con el comportamiento de una variable local. program usoDeVariablesEstaticas; procedure pLocalYEstatica; const vEstatica : Integer = 1; var vLocal: Integer; begin vLocal := 1; writeln(vEstatica:4, vLocal:9); inc(vEstatica); inc(vLocal); end; var i:Integer; begin writeln(' Estatica Local'); for i:=1 to 5 do begin write(i:2, ') '); pLocalYEstatica; end; end. La ejecución de este programa dará como resultado lo siguiente:
129
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Observe que la variable estática mantiene su valor aun cuando se termine la ejecución del subprograma, esto no pasa con la variable local que se destruye cuando termine el procedimiento y se vuelve a crear cuando se le vuelve a llamar.
Parámetros por valor y por referencia Al igual que se requieren que se ingrese información a los programas para que estos se puedan ejecutar de acuerdo a las necesidades del momento, en la mayoría de los casos se requiere introducir información a los subprogramas para que estos puedan realizar su trabajo. Piense en la función sin o en procedimiento write, ambos requieren de datos para realizar su trabajo, sin requiere del valor del ángulo que queremos calcular, write a su vez necesita de los expresiones que deseamos mostrar en la pantalla. Esta información, que es introducida a los subprogramas se denomina parámetros o argumentos. Existen dos tipos de parámetros en un subprograma, los parámetros por valor y los parámetros por referencia, estos tipos de parámetros serán estudiados a continuación. Parámetros por valor: un parámetro por valor se declara en un subprograma en Pascal de la siguiente manera: …
(
:
Identificador
Ti o de dato
=
…
, ;
Según esta sintaxis, estos parámetros se pueden definir como se muestran en los ejemplos siguientes: function factorial (n: Integer): LongInt; var … begin … end; procedure estadoDeCuentas(nombre, apellidoPat, apellidoMat: String); … prodedure imprimeDatos(cod: LongInt, nomb, apellPat, apellMat: String; edad: Integer; sueldo: Real); … Ahora veamos cómo trabaja y lo que significa un parámetro por valor. El siguiente programa permite calcular el factorial de un número: {01} program calculaElFactorialDeUnNumero; {02} function factorial(n: Integer): LongInt; {03} var i: Integer; {04} fact: LongInt; {05} begin {06} fact := 1; {07} for i := n downto 1 do {08} fact := fact * i; {09} factorial := fact; {10} end; 130
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
{11} var a: Integer; {12} f: LongInt; {13} begin {14} write('Ingrese un número: '); {15} readln(a); {16} f := factorial(a); {17} writeln('Factorial de ',a ,' = ', f); {18} end. La ejecución del programa dará como resultado:
El programa se ejecuta de la siguiente manera: 1. La ejecución empieza en la línea {11} y {12}, allí se declaran las variables a y f relacionándose con sendas direcciones de memoria, la siguiente gráfica muestra a está situación: f
∞
∞
2. Luego se sigue hasta la línea {15} en donde se lee un valor para la variable a, colocándose ese valor en la posición de memoria relacionada con a, en el ejemplo se lee el valor de 5. Entonces:
a
f
5
∞
3. La siguiente línea ({16}) es el llamado a la función factorial, el programa toma el valor de la variable a y lo envía a la función. La función recibe ese valor y para poder manejarlo requiere almacenarlo en alguna parte de la memoria, por eso que en el encabezado de la función se colocan los parámetros, son allí donde se colocarán los valores enviados a la función. En el caso de la función factorial (function factorial(n: Integer): LongInt;), es el parámetro n el que recibe el valor de 5. El mapa de memoria del programa es ahora como se muestra a continuación:
n
a 5
Ámbito de las variables de la función factorial
f 5
∞
Ámbito de las variables del programa principal
El nombre que se da a los parámetros de un subprograma no depende del programa principal, por eso se puede dar al parámetro cualquier nombre, incluso se le puede dar el mismo nombre que el de la variable que envía el valor. Así por ejemplo, en 131
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
vez de n se le pudo dar el nombre de a al parámetro y esto no va a cambiar el comportamiento del programa. Esta manera de manejar la información entre los subprogramas permite que cualquier modificación que se haga al contenido del parámetro, no afectará a la variable que envía el valor al subprograma, esto es si modificáramos el código de la función factorial por: function factorial(a: Integer): LongInt; var i: Integer; fact: LongInt; begin fact := 1; for i := a downto 1 do fact := fact * i; a := a + 100; factorial := fact; end; El resultado que se obtendrá es el mismo a pesar que el valor de la variable a fue modificado. Otra característica de los parámetros por valor es que a los subprogramas se les puede enviar no sólo el valor contenido en un variable, sino también se les puede enviar un valor constante o el producto de una expresión. Así, el programa principal podría modificarse de la siguiente forma: var a: Integer; f: LongInt; begin write('Ingrese un número: '); readln(a); f := factorial(a); writeln('Factorial de ', a,' = ', f); {Empleando un valor constante} f := factorial(7); writeln('Factorial de 7 = ', f); {Empleando una expresión} f := factorial((a + 3) div 2); writeln('Factorial de 4 = ', f); end. Obteniéndose los siguientes resultados:
132
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Parámetros por referencia: el comportamiento de un parámetro por referencia es muy diferente al de un parámetro por valor; primero su declaración cambia, se declara en un subprograma en Pascal de la siguiente manera: …
(
var
:
Identificador
Ti o de de dat datoo
)
…
, ;
Según esta sintaxis, estos parámetros se pueden definir como se muestran en los ejemplos siguientes: procedure incrementar incrementar((var var valor valor:: Integer Integer); ); … prodedure leerDatos leerDatos((var arch arch:: Text Text;; cod:: LongInt cod LongInt;; nomb,, apellPat nomb apellPat,, apellMat:: String apellMat String;; var edad edad:: Integer Integer;; var sueldo sueldo:: Real Real); ); … Ahora veamos cómo trabaja y lo que significa un parámetro por referencia. El siguiente programa permite incrementar el valor de una variable que se ingresa como parámetro: {01} Program ilustraElUsoDePrarametrosProReferencia ilustraElUsoDePrarametrosProReferencia;; {02} procedure incrementar incrementar((var v : Integer Integer); ); {03} var incremento incremento:: Integer Integer;; {04} begin {05} write(('Ingrese el valor del incremento: '); write '); {06} readln((incremento readln incremento); ); {07} v := := v + + incremento incremento;; {08} end end;; {09} var valor valor:: Integer Integer;; {10 } begin {11} write(('Ingrese un valor entero: '); write entero: '); {12} readln((valor readln valor); ); {13} incrementar((valor incrementar valor); ); {14} writeln(('El nuevo valor es: ', writeln ' , valor valor); ); {15} end end.. La ejecución del programa dará como resultado:
El programa se ejecuta de la siguiente manera: 1. La ejecución empieza en la línea {09} {09},, allí se declaran las variables valor relacionándola con una dirección de memoria, la siguiente gráfica muestra está situación: valor
∞ 133
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
2. Luego se sigue hasta la línea {12} en donde se lee un valor para la variable valor valor,, colocándose ese valor en la posición de memoria relacionada con esa variable. En el ejemplo se lee el valor de 259. Entonces:
valor
259 3. La siguiente línea {13}) ({13}) es el llamado al procedimiento incrementar incrementar;; en este punto el programa no envía el valor de la variable valor valor al procedimiento, sino envía la dirección de memoria relacionada con la variable valor valor.. El procedimiento recibe esa dirección de memoria y con ella define el parámetro v , relacionándola con la dirección recibida (que coincide con la de la variable valor valor). ). En ese momento resulta que tanto la variable valor valor como como la variable v estarán estarán relacionadas al mismo espacio de memoria, es como si la variable valor valor reciba reciba un “alias” y durante la ejecución del subprograma la variable valor valor se se denominara v . Luego en la línea {03} línea {03} se define una nueva variable, incremento incremento.. El mapa de memoria del programa es ahora como se muestra a continuación:
valor
v
incremento
259
∞ Ámbito de las variables de la función factorial
Ámbito de las variables del programa principal
Al igual que con los parámetros por valor, el nombre que se da a los parámetros de un subprograma no depende del programa principal, por eso se puede dar al parámetro cualquier nombre, incluso se le puede dar el mismo nombre que el de la variable que envía el valor. Así por ejemplo, en vez de v se se le pudo dar el nombre de valor valor al al parámetro y esto no va a cambiar el comportamiento del programa. 4. En la línea {06} se lee un valor para la variable incremento incremento,, en el ejemplo se le asigna 87. Luego en la línea {07} línea {07},, se hace una modificación a la variable v , allí se le agrega el valor de incremento incremento;; aquí se modifica el contenido de la dirección de memoria relacionada con la variable v , quedando de la siguiente manera:
v
incremento ∞
Ámbito de las variables de la función factorial
valor 346 Ámbito de las variables del programa principal
Al terminar la ejecución del subprograma, tanto el parámetro v como la variable incremento son incremento son destruidas por el sistema quedándose sólo la variable valor valor,, con su 134
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
valor modificado; es por eso que cuando la variable valor valor es es impresa en la línea {14} línea {14} se muestra 346 y no el valor original orig inal de 259. Los parámetros por referencia constituyen la manera de obtener más de un resultado en un subprograma.
Solución de problemas empleando diseño descendente y programación modular A continuación se presenta una serie de problemas que ilustran esta metodología. 1. Se desea escribir un programa que permita imprimir las facturas que se han elaborado por las ventas realizadas en una tienda. Para realizar esta labor, se cuenta con tres archivos de texto con la información requerida. El primer archivo, denominado “producto.txt”, contiene la información de todos los productos que comercializa la tienda; este archivo al ser de tipo texto, y con la finalidad de simplificar la solución del problema, de modo que nos podamos concentrar en la metodología, es similar al que se muestra a continuación: Archivo de Productos: 123456 Sillón de cuero rojo 1199.90 345678 Cocina de 4 hornillas 345.50 626262 Lavadora 689.99 …
Código del producto Descripción Precio unitario
En este archivo, agrupadas de tres en tres líneas, se encuentra el código del producto, la descripción del producto y el precio unitario del mismo. El segundo archivo, denominado “clientes.txt”, contiene la información de todos los clientes que han comprado alguna vez algún producto en la tienda. De manera similar que en el archivo anterior, el archivo es parecido al que se muestra a continuación: Archivo de Clientes: 23764590 Pedro García López 45667728 Manuel Paredes Gómez 81300045 Gustavo Tapia Ruiz …
DNI del cliente Nombre del cliente
En este archivo se encuentra en líneas independientes, el código del DNI del cliente y el nombre del mismo. El tercer archivo contiene la información de las facturas que se han emitido en la tienda en un periodo de tiempo. A diferencia de los otros que siempre serán los mismos y que solamente se agregarán datos cada cierto tiempo, este archivo contiene los datos de las facturas emitidas en un período de tiempo, una vez que termina el período, se crea un nuevo archivo sin borrar el anterior, esto para poder tener un mejor control del los productos vendidos. Por esta razón no se conoce el nombre del 135
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
archivo; cada vez que se ejecute la aplicación se deberá solicitar al usuario el nombre del archivo con el que se va a trabajar. El archivo contiene en cada línea la información necesaria para poder elaborar cada factura, esto es: en número de factura, el DNI del cliente que hizo la compra, y la lista de productos comprados, aquí sólo se consigna el código y la cantidad comprada de cada producto, pudiendo haber muchos productos en una línea. El archivo es similar al que se muestra a continuación: Archivo de Facturas: 10001 81300045 445566 5 89098899 10 … 10002 23764590 345678 3 10036 11223344 626262 1 445566 21 … …
Código de la factura, DNI del cliente, código y cantidad del producto
Finalmente, el programa deberá imprimir las facturas de una manera similar a lo que se muestra a continuación: No. De Factura: 10001 Cliente: Juan Pedro López Pérez Código Descripción 123453 Pintura mate 545454 Brocha #3 101022 Escalera 2 m
Precio Unitario 25.40 3.80 156.30
Cantidad 4 2 1 Total:
Subtotal 101.60 7.60 156.30 265.50
Solución: Al tratarse de un problema complejo. La solución debe plantearse empleando Solución: un diseño descendente, modulando el programa mediante funciones y procedimientos. El aplicar el diseño descendente nos permitirá ir encontrando la solución en el camino e ir escribiendo el código del programa aun así no tengamos una idea muy clara de cada detalle de la solución completa. Primero analicemos a grandes rasgos las tareas que hay que realizar para elaborar el problema; por un lado se tendrá que manejar esos archivos descritos en el problema, luego una tarea que debemos hacer es preparar los archivos (asignarlos, abrirlos, etc.) para que podamos trabajar con ellos, luego que los archivos estén abiertos habrá que procesar los datos para conseguir la impresión de las facturas, finalmente se deberá cerrar esos archivos. Entonces escribamos estas tareas como primer paso al desarrollo del problema: Tareas: 1- Prepara los archivos 2- Procesar los datos e imprimir las facturas 3- Cerrar los archivos
Luego debemos analizar qué información se requiere para realizar cada tarea o qué información debe devolver la tarea para que se de por satisfecha su ejecución. Entonces, para la primera tarea podemos apreciar que no se requiere enviarle información a la tarea ya que los datos que se requieren para abrir los archivos son constantes (los nombres de los archivos) o se solicitarán al usuario; lo que sí, la tarea debe devolver las variables de archivo debidamente asignadas para que las otras tareas se puedan realizar. 136
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
La segunda y tercera tarea requiere de las variables de archivo, la primera para poder procesar los datos y la tercera para poder cerrar los archivos, luego se les deberá proporcionar estos datos. Una vez hecha la anotación, pasaremos a formalizar las tareas escribiendo el código correspondiente, para esto cada tarea será considerada como un procedimiento o función: program imprimeFacturas imprimeFacturas;; {Programa principal} var archProd archProd,, archCli archCli,, archFact archFact:: Text Text;; begin prepararArchivos((archProd prepararArchivos archProd,, archCli archCli,, archFact archFact); ); imprimirFacturas((archProd imprimirFacturas archProd,, archCli archCli,, archFact archFact); ); cerrarArchivos((archProd cerrarArchivos archProd,, archCli archCli,, archFact archFact); ); end.. end En este momento el problema no ha sido solucionado en su totalidad, sin embargo ya se dividió en tres partes, la primera y tercera son tareas muy simples, la segunda es mucho más compleja, más aun, todavía no tenemos muy claro cómo la vamos a solucionar. Por esto nos vamos a concentrar primero en las más simples. El procedimiento prepararArchivos prepararArchivos se se encargará de asignar a las variables de archivo la información necesaria para poder manejar los archivos, por regla general las variables de archivo siempre deben pasar por referencia. En el cuerpo del procedimiento, la tarea que se debe realizar es la asignación a las variables de archivo de las referencias a los archivos físicos, habrá que tomar en cuenta que dos de los archivos son siempre los mismos y el tercero se debe preguntar al usuario el nombre del archivo con el que se va a trabajar en ese momento. La tarea de apertura de los archivos no se considerará en este módulo debido a que esto, como se verá más adelante, se deberá realizar varias veces en el programa El código será el siguiente: procedure prepararArchivos (var archProd, archCli, archFact: Text); var nombArchFact: String; begin assign(archProd, ‘producto.txt’); assign(archCli, ‘clientes.txt’); write('Nombre del archivo de facturas: '); readln(nombArchFact); assign(archFact, nombArchFact); end; El procedimiento cerrarArchivos cerrarArchivos se encargará sólo del cierre de los archivos, la implementación es inmediata. procedure cerrarArchivos(var archProd, archCli, archFact: Text); begin close(archProd); close(archCli); close(archFact); end; 137
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
El procedimiento imprimirFacturas es más complejo por lo que debemos aplicar un diseño descendente. Primero debemos analizar los archivos con los que contamos; al realizar esto vemos que los archivos “producto.txt” y “clientes.txt” son archivos que no tienen relación con los demás archivos, estos archivos se denominan entidades, por ejemplo clientes.txt guarda la información sólo de los clientes de la tienda, aquí no hay ningún campo que lo relacione con los productos, sólo con este archivo no se puede saber qué productos compró un cliente o en qué momento; esto es similar con el archivo de productos. Sin embargo, la información que guarda el archivo de facturas es diferente, al obtener los datos de una línea del archivo podemos saber quién la compró y qué productos compró; sin embargo no está completa, la información del cliente se da sólo por el DNI, mas no por su nombre (dirección, teléfono, etc.) y en cuanto a los productos que compró, sólo tiene el código y cantidad de los productos que compró, no está lo demás. Este tipo de archivo se conoce con el nombre de relaciones, ya que si es cierto que no están completos, la información que contienen es suficiente para poder, en conjunto con los otros, completar la factura. Entonces, según lo expuesto, de nada nos sirve en el programa empezar a trabajar por los archivos de productos o clientes porque no vamos a poder relacionarlos con los otros, debemos empezar el análisis por el archivo de relación facturas.txt. Las tareas que vamos a realizar son: Tareas: 1- Mientras hayan datos en el archivo de facturas 1.1- Leer el código de una factura y el DNI del cliente 1.2- Buscar en el archivo de clientes el nombre del cliente 1.3- Imprimir los datos de la factura y del cliente 1.4- Imprimir los productos que compró
Observe que estamos tratando de definir las tareas, no de dar solución completa al programa. Observe que la tarea 1.4 es compleja y que no hemos analizado aún cómo la vamos a solucionar. La implementación de este módulo será como sigue: procedure imprimirFacturas(var archProd, archCli, archFact: Text); var numFact, dni: Longint; cliente: String; begin reset(archFact); while not eof(archFact) do begin read(archFact, numFact, dni); cliente := buscaCliente(dni, archCli); {esta función devuelve el nombre del cliente, pero si hay un error en el DNI y no se encuentra éste en el archivo de clientes, se envía un texto que indique que la operación no fue satisfactoria} if cliente <> ‘NOENCONTRADO’ then begin {Si lo encuentra, imprime encabezado de factura} writeln('Factura No. ', numFact); writeln('Cliente: ', cliente); imprimeProductos(archFact, archProd); end else 138
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
writeln('DNI: ', dni, ‘ no se encuentra en el archivo’); readln(archFact); writeln(‘==============================’); end;
end; En este módulo observamos dos tareas inconclusas, buscaCliente e imprimeProductos, habrá que darle solución a cada uno. En el caso de buscaCliente, se deberá tomar desde el inicio del archivo de clientes uno a uno los datos hasta ubicar el DNI buscado, este proceso deberá ser secuencial porque no se sabe si está o no ordenado el archivo por DNI. La búsqueda debe hacerse siempre desde el inicio del archivo, por esto es que la apertura del archivo la realizamos dentro de esta función ya que de no hacerlo así, cuando se busque el cliente de la segunda factura, éste será buscado no desde el principio sino desde donde se quedó en la búsqueda anterior. El código ser function buscaCliente(dni: Integer; var archCli: Text): string; var dniCli: Longint; nombCli: String; encontrado: Boolean; begin reset(archCli); {nos colocamos al inicio del archivo} encontrado := false; while not eof(archCli) and not encontrado do begin readln(archCli, dniCli); readln(archCli, nombCli); encontrado := dniCli = dni; {si el DNI buscado es igual al que leímos del archivo, se asigna true al la variable encontrado y provoca la salida del while, de lo contrario asigna false y se sigue iterando} end; if encontrado then buscaCliente := nombCli else buscaCliente := 'NOENCONTRO'; end; El procedimiento imprimeProductos deberá continuar la lectura de los datos de la factura que estamos imprimiendo, luego habrá que leer el código y cantidad de un producto y con esos datos completar la información (descripción y precio unitario) para luego calcular el subtotal y finalmente poderlo imprimir en la factura. Este proceso se debe repetir hasta que se termine la línea de datos. La lista de tareas por lo tanto será: Tareas: 1- Mientras hayan datos en la línea de la factura 1.1- Leer el código y la cantidad de un producto 1.2- Buscar en el archivo de productos la descripción y el precio unitario des producto 1.3- Calcular el subtotal del producto comprado (precio unitario x cantidad comprada) 1.4- Acumular el subtotal 1.5- Imprimir los datos del producto
El código correspondiente es el siguiente: 139
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedure imprimeProductos(var archFact, archProd: Text); var codProd: Longint; cant: Integer; precUnit, subTotal, total: Real; desc: String; begin total := 0; while not eoln(archFact) do begin read(archFact, codProd, cant); buscaDescYPrec(codProd, archProd, desc, precUnit); {Observe aquí que la búsqueda se hace a través de un procedimiento y no de una función como en el caso del cliente, esto se hace debido a que la tarea requiere devolver dos datos (descripción y precio unitario) y no uno como en el cado del cliente} if desc <> 'NOENCONTRO' then begin subtotal := cant * precUnit; total := total + subTotal; writeln(codProd, desc:30, precUnit:8:2, cant:3, subTotal:10:2); end else writeln(codProd,' **** No está en el archivo ***'); end; writeln('Total: ',total:10:2); end; Finalmente el proceso de búsqueda del producto se realizará de manera muy similar a la del cliente, con la salvedad que en este caso se trata de un procedimiento y no de una función. Los parámetros devueltos (desc y precUnit) deberán ser definidos como parámetros por referencia. procedure buscaDescYPrec (codProd: Longint; var archProd: Text; var desc: String; var precio: Real); var cProd: Longint; encontrado: Boolean; begin reset(archProd); encontrado := false while not eof(archProd) and not encontrado do begin readln(archProd, cProd); readln(archProd, desc); readln(archProd, precio); encontrado := cProd = codProd; end; if not encontrado then desc := 'NOENCONTRO'; end; 2. Se tiene un archivo de datos que contiene las notas obtenidas por los alumnos en diferentes cursos. El archivo tiene una estructura similar a la siguiente: 140
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo Técnicas de programación 0 20 Juan Pérez Ruiz 10 14 7 123 19 11 -12 9 1234 17 Cecilia Rojas Ramos 11 24 15 12 13 10 12 178 ... Paula Castillo Sánchez 18 20 -12 34 14 1111 77 17 -21 *** 13.45293 Lenguajes de Programación 1 15 58 Alexandra Neyra Núñez
Nombre del curso 1 Rango de notas Nombre del alumno 1 Notas del alumno 1 Nombre del alumno 2 Notas del alumno 2 Nombre del alumno n Notas del alumno n Tres asteriscos que indican el fin de los alumnos del curso 1 Promedio del curso del semestre anterior Nombre del curso Rango de notas Nombre del alumno 1
…
Se desea que usted elabore un programa que permita leer este archivo y que genere otro archivo en el que aparezca la información resumida del primero con la siguiente estructura: Técnicas de programación 0 20 Juan Pérez Ruiz 11.460 Cecilia Rojas Ramos 11.800 ... Paula Castillo Sánchez 17.000 *** -2.367% Lenguajes de Programación 1 15 58 Alexandra Neyra Núñez 17.346 …
Nombre del curso Rango de notas Nombre y promedio del alumno 1 Nombre y promedio del alumno 2 Nombre y Notas del alumno n Tres asteriscos que indican el fin d e los alumnos del curso 1 Porcentaje de variación del promedio del curso con respecto al semestre anterior Nombre del curso Rango de notas Nombre del alumno 1
El promedio debe calcularse de acuerdo a lo siguiente: o Las notas que estén fuera de rango corresponden a evaluaciones especiales y no deben ser consideradas en el promedio. o Cada alumno puede tener un número diferente de evaluaciones. o Las dos últimas evaluaciones válidas corresponderán al primer y segundo examen, las demás notas válidas corresponden a las notas de prácticas. o La fórmula a emplear será: Prom = (3xPP + 3xEx1 + 4xEx2)/10, donde PP es el promedio de notas de práctica descartando la menor nota. Solución: Al igual que en el problema anterior, la solución se dará empleando un diseño descendente. Luego, analizando el problema, observamos que también se debe trabajar sobre archivos (para el ingreso y la salida de datos) por lo que una tarea será la de preparar estos archivos y al final otra tarea será la de cerrarlos. En el intermedio debemos procesar el archivo; esta tarea también es simple, debido a que el archivo está organizado en bloques por cursos y cada bloque empieza con el nombre del curso seguido por el rango de notas válidas y finalmente un bloque con los alumnos del curso, la secuencia de tareas sugeridas podrá ser la siguiente: Tareas: 1- Preparar los archivos 2- Mientras hayan datos para un cursos en el archivo 2.1- Leer el nombre del curso e imprimirlo en el archivo de salida 1.2- Leer el rango de notas e imprimirla en el archivo de salida 1.3- Procesar las notas de cada alumnos del curso 3- Cerrar los archivos
141
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
El código para estas tareas será: program resumenDeCursos; var archIn, archOut: Text; {ArchIn manejará el archivo de datos (archivo de ingreso), mientras archOut manejará el archivo que contendrá el reporte final solicitado} nombCur: String; limInf, limSup: Integer; promCur, promAnt, variac: Real; begin prepararArchivos(archIn, archOut); while not eof(archIn) do begin readln(archIn, nombCur); writeln(archOut, nombCur); readln(archIn, limInf, limSup); writeln(archOut, limInf:4, limSup:4); procesaCurso(archIn, archOut, limInf ,limSup); end; cerrarArchivos(archIn, archOut); end. procedure preparaArchivos(var archIn, archOut: Text); var nombArchIn, nombArchOut: String; begin {No se indican los nombres de los archivos, por lo que se tendrán que solicita al usuario} write (‘Ingrese el nombre del archive de datos: ‘ ); readln(nombArchIn); assign(archIn, nombArchIn); reset(archIn); write (‘Ingrese el nombre del archive para el reporte: ’); readln(nombArchOut); assign(archOut, nombArchOut); rewrite(archOut); end; procedure cerrarArchivos(var archIn, archOut: Text); begin close(archIn); close(archOut); end; Para realizar la tarea de procesar las notas de los alumnos del curso, vemos que en el archivo los datos de cada alumno se encuentran en dos líneas, en la primera está el nombre del alumno, y en la segunda la lista de valores de los cuales hay que sacar sólo las notas dentro de los límites. La lista de alumnos se marca con una línea en la que hay tres asteriscos (***). Las tareas para realizar esta labor serán como se indican: 142
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Tareas: 1- Leer el nombre de un alumno y escribirlo en el archivo de salida 2- Si el nombre del alumno corresponde a ‘***’, terminar el proceso (ir a 7) 3- Procesar la línea de notas y calcular el promedio del alumno 4- Acumular el promedio para determinar el promedio general del curso 5- Incrementar la cuenta de alumnos del curso 6- Volver al paso 1 7- Calcular la variación del promedio con el anterior semestre
El procedimiento correspondiente a esta tarea será el siguiente: procedure procesaCurso( var archIn, archOut: Text; limInf, limSup: Integer); var nombAlum: String; numAlum: Integer; suma, prom, promCur, promAnt, variac: Real; begin numAlum:=0; suma:=0; repeat readln(archIn, nombAlum); write(archOut, nombAlum); if (nombAlum <> '***' ) then begin prom := promedio(archIn, limInf, LimSup); {Esta tarea se realizará con una función debido a que sólo interesa que se devuelva el promedio obtenido} writeln(archOut, prom:9:3); suma := suma + prom; inc(numAlum); end; until nombAlum = '***'; writeln(archOut); promCur := suma / numAlum; readln(archIn, promAnt); {Leemos el promedio anterior} variac := (promCur - promAnt) / promCur * 100; writeln(archOut,variac:9:3,'%'); end; Para realizar la tarea de calcular el promedio se debe tomar en cuenta que los datos están todos en una línea. Por otro lado hay valores que estarán fuera del rango, y que por lo tanto no se deben tomar en cuenta para el cálculo del promedio. Finalmente está el caso de los exámenes, debido a que hay notas que pueden estar fuera del rango, no se puede afirmar que las dos últimas corresponderán a los exámenes, para poder determinar qué notas corresponden a los exámenes se empleará un algoritmo similar al que se usó para el cálculo de la serie Fibonacci, esto es, se guardarán los dos últimos valores válidos luego de cada lectura. Las tareas para realizar este proceso serán como sigue: Tareas: 1- Inicializamos las notas del los exámenes en cero y la nota mínima de prácticas con el
143
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo valor del límite superior 2- Mientras hayan valores en una línea 2.1- Leer una nota 2.2- Si el valor está dentro del rango 2.2.1- Incrementar el número de notas 2.2.2- La nota leída es la última leída hasta ese momento, por lo tanto podría ser el examen final, por lo tanto la nota registrada hasta ese instante para el primer examen corresponde a una práctica, la nota registrada para el segundo examen corresponde al primer examen y la nota leída debe correspondes con la del segundo examen. 2.2.3- Si el numero de notas leído hasta ese momento es mayor a dos 2.2.3.1- Si la nota de práctica es menor que la mínima registrada entonces se reemplaza la nota mínima por la de la práctica 2.2.3.2- Acumular la nota de práctica 3- Calcular el promedio del alumno con los datos determinados
function promedio(var archIn: Text; limInf, LimSup: Integer): Real; var ex1, ex2, pract, min, sumaPr, nota, numNotas: Integer; begin numNotas := 0; ex1 := 0; ex2 := 0; sumaPr := 0; min := limSup; while not eoln(archIn) do begin read(archIn, nota); if (nota >= limInf) and (nota <= limSup) then begin inc(numNotas); pract := ex1; ex1 := ex2; ex2 := nota; if numNotas > 2 then begin if pract < min then min := pract; sumaPr := sumaPr + pract; end end; end; readln(archIn); promedio := (3*(sumaPr-min)/(numNotas-3) + 3*ex1 + 4*ex2)/10; {Se resta tres (3) al número de notas porque se han contado todas las notas, incluyendo la de los exámenes (2) y la de la nota mínima de prácticas} end; 3. Se tienen un archivo de textos similar al que se muestra a continuación: 123 3AF4 10H31 2457 152 HOLA ABF G3214 67J 56FA XGEW 12345 98 14583 …
D 876GRF
87
4B00F 6TYF 87AB
Se desea que usted elabore un programa que permita leer inicialmente dos números enteros, ambos en el rango de [2, 30], los cuales deberá validar. Los dos números indicarán una base inicial y una base final. Lego el programa deberá leer caracter por caracter el archivo de textos y detectar todos aquellos números que estén escritos en la base inicial y descartar los que no lo estén. 144
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Una vez que se compruebe que el número está en la base inicial, deberá imprimirse éste en otro archivo, pero en la base final. Al término de cada línea deberá imprimir el promedio de los números válidos, también en la base final. Por ejemplo, si la base inicial fuera 16 y la base final 8, la respuesta para el archivo anterior sería: 443 35364 22127 522 15 207 Promedio: 10143 5277 Promedio: 5277 53372 221505 230 242603 1130017 103653 Promedio: 252003 …
Se descartó: 10H31, 876GRF Se descartó: HOLA, G3214, 67J Se descartó: XGEW, 6TYF,
Si la base inicial hubiera sido 8 y la base final 16, la respuesta para el archivo anterior sería: 53 52F 6A Promedio: 1F9 Promedio: 0 14E5 Promedio: 14E5 …
Se descartó: 3AF4, 10H31, D, 876GRF, 87 Se descartó: HOLA, ABF, G3214, 67J Se descartó: 56FA, XGEW, 4B00F, 98, 14583, 6TYF, 87AB
Solución: Al igual que en el problema anterior vemos que también se debe trabajar sobre archivos por lo que se debe realizar tareas para preparar y cerrar los archivos. La tarea principal consistirá en leer las bases inicial y final, verificando que estén en el rango establecido, y luego empezar a leer las palabras del archivo; aquí se debe verificar si la palabra corresponde con un número en la base inicial, si no es así se descarta, de lo contrario se acumula este valor para el cálculo del promedio y se escribe el número en la base final. Al detectar el final de la línea se debe imprimir el promedio para la línea y seguir con la siguiente línea, esto hasta terminar con el archivo. Las tareas, por lo tanto se muestran a continuación: Tareas: 1- Preparar los archivos 2- Leer la base inicial y final verificando el rango 3- Mientras hayan datos en el archivo 3.1- Leer una palabra 3.2- Si la palabra corresponde a un número en la base inicial imprimir el número en la base final y acumularlo. 3.2- Si se llegó al final de la línea se imprimirá el promedio de la línea en la base final y se reiniciarán los contadores. 4- Cerrar los archivos
El código para estas tareas será como sigue: Program promediosEnDiferentesBases; const limInf = 2; limSup = 30; var archIn, archOut: Text; baseIni, baseFin, numDat: Integer; num, suma: Longint; esCorrecto, hayFinDeLinea: Boolean; begin preparaArchivos(archIn, archOut); writeln(‘Ingresar la base inicial: ‘); leerBase(baseIni); writeln(‘Ingresar la base final: ‘); leerBase(baseFin); suma:=0; numDat:=0; {inicializamos las variables para el promedio} 145
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
while not eof(archIn) do begin leeNum(archIn, baseIni, num, esCorrecto, hayFinDeLinea); {leeNum es un procedimiento que leerá una palabra del archivo, este procedimiento verificará si la palabra corresponde a un número en la base inicial y lo almacenará en la variable num. La variable esCorrecto devuelve el valor de verdadero si el número asignado estaba en la base inicial y falso en caso contrario. La variable hayFinDeLinea devolverá verdadero si se terminó la línea luego de leer la palabra} if esCorrecto then begin imprime(archOut, num, baseFin); suma := suma + num; inc(numDat); end; if hayFinDeLinea then begin write(archOut,' Promedio: '); if numDat = 0 then write(archOut, 0) else imprime(archOut, suma div numDat, baseFin); {usamos el mismo procedimiento imprime que se usó para los números} writeln(archOut); suma := 0; numDat := 0; end; end; cerrarArchivos(archIn, archOut); end. procedure prepararArchivos(var archIn, archOut: Text); var nombArchIn, nombArchOut: String; begin write(‘Ingrese el nombre del archivo de datos: ’); readln(nombArchIn); assign(archIn, nombArchIn); reset(archIn); write(‘Ingrese el nombre del archivo para el re´porte : ’); readln(nombArchOut); assign(archOut, nombArchOut); rewrite(archOut); end. procedure cerrarArchivos(var archIn, archOut: Text); begin close(archIn); close(archOut); end. 146
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedure leerBase(var base: Integer); begin repeat write(‘Ingrese un numero entre ‘, limInf, ‘ y ‘, limSup, ‘: ’); readln(base); until (base >= limInf) and (base <= limSup); end. La tarea de leer una palabra y transformarla en un número debe realizarse caracter por caracter, luego de leer un caracter debe verificarse si éste corresponde a una cifra que pertenezca a la base inicial, si pertenece entonces se debe transformar el caracter en una cifra numérica y hacerlo formar parte del número. Si el caracter no está en la base inicial se debe verificar si este caracter corresponde a un espacio en blanco o un cambio de línea si es así el número terminó de leerse, si no se verifica que la palabra no corresponde a la base inicial. Si el caracter leído corresponde a un cambio de línea entonces se debe registrar en la variable correspondiente. El código se presenta seguidamente: procedure leeNum( var archIn: Text; base: Integer; var num: Longint; var esCorrecto, hayFinDeLinea: Boolean); var c: Char; dig: Integer; begin esCorrecto := true; hayFinDeLinea := false; num := 0; repeat read(archIn, c); if not (c in [' ', #10, #13]) then begin {Convertimos el caracter en un número} if c in ['0'..'9'] then dig := ord(c) - ord('0') else dig := ord(c) - ord('A') + 10; if (dig >= 0) and (dig < base) then num := num *base + dig else esCorrecto := false; end; until c in [' ', #10, #13]; if c in [ #10, #13] then begin hayFinDeLinea := true; read(archIn,c); {Leemos el segundo caracter del cambio de línea} end; end; La tarea de imprimir el número en la base final es similar al que se vio en capítulos anteriores, por eso sólo mostraremos el código: procedure imprime(var arch: Text; num: Longint; base: Integer); var factor: Longint; 147
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
cifra: Integer; begin write(arch,' '); factor := 1; while factor <= num do factor := factor * base; while (factor div base) <> 0 do begin factor := factor div base; cifra := num div factor; if cifra < 10 then write(arch, cifra) else write(arch, chr(cifra – 10 + ord('A'))); num:= num mod factor; end end;
148
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 7: Aplicaciones con arreglos En este capítulo empezaremos a estudiar los tipos de datos estructurados, en particular, estudiaremos aquí los arreglos. Para recordar este concepto diremos que un dato estándar es aquel que define un solo elemento, por ejemplo si definimos una variable de la siguiente manera: var a: Integer; sabemos que en la variable a podemos almacenar sólo un valor, y así ha sido en todos los casos que hemos analizado hasta ahora. A diferencia de los datos estándar, los datos estructurados permiten definir variables en las que en vez de almacenar sólo un valor, podremos almacenar en ellas muchos valores. La presencia de datos estructurados en los lenguajes de programación se debe a que existen muchos problemas en los que si sólo se emplearan datos estándar las soluciones serían muy ineficientes y difíciles de manejar y mantener. Analicemos los siguientes casos que se pueden presentar: - Supongamos que tenemos un conjunto de datos de los cuales deseamos obtener su desviación estándar. Una fórmula que permite calcular este valor es: σ = ∑ ( x − x ) n
2
i =1
i
n −1
-
Esta fórmula nos obliga a calcular previamente el promedio ( x ) de todos los datos para luego poder determinar la desviación estándar. Si es cierto que la solución la podemos plantear empleando únicamente variables estándar, esta solución no sería eficiente. La razón para esto se debe a que como debemos calcular primero el promedio, tendíamos que leer los datos uno a uno y acumularlos en una suma, como se muestra a continuación: while not eof(arch) do begin read(arch, dato); suma := suma + dato; inc(numDat); end; prom:= suma/numDat; Una vez hecho esto, para poder ahora determinar la desviación estándar debemos calcular otra sumatoria en la que restemos cada dato del promedio, lo elevemos al cuadrado y finalmente se le agregue a la sumatoria. Sin embargo, como ya no tenemos los datos en memoria, no nos va a quedar otra cosa que hacer que volver a leer todos los datos. Esto ya torna ineficiente la solución. Si tuviéramos una forma de almacenar los datos leídos en memoria la primera vez que los leemos, entonces, luego de calcular el promedio, cuando queremos calcular la segunda sumatoria, los datos se tomarían directamente de la memoria y no los tendríamos que leer nuevamente, esto sería muchísimo más rápido y por lo tanto haría más eficiente el proceso. Otro problema que se presenta y que ilustra muy bien la necesidad del uso de variables estructuradas es el de la determinación de una distribución de frecuencias. Supongamos que se tiene un archivo en el que se ha registrado la intención de voto de un grupo de personas en una futura elección, supongamos que hay cinco candidatos y cada uno se identifica en el archivo por un número entero entre 1 y 5. A partir de 149
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
este archivo deseamos saber qué porcentaje quiere votar por el primer candidato, qué porcentaje para el segundo, etc. Empleando variables estándar, la solución podría encaminarse a la definición de cinco variables, las cuales acumularían la cantidad de simpatizantes que votaría por cada candidato. El programa sería más o menos como sigue: program distribucionDeFrecuemncias; var canad1, canad2, canad3, canad4, canad5: Integer; voto, total: Integer; porc1, porc2, porc3, porc4, porc5: Real; … begin … {Inicializamos los contadores} canad1 := 0; canad2 := 0; canad3 := 0; canad4 := 0; canad5 := 0; {Leemos los datos y los acumulamos dependiendo del valor leído} while not eof(arch) do begin read(arch, voto); if voto = 1 then inc(candid1); else if voto = 2 then inc(candid2); if voto = 3 then inc(candid3); if voto = 4 then inc(candid4); if voto = 5 then inc(candid5); end; total := canad1 + canad2 + canad3 + canad4 + canad5; porc1 := candid1 / total; … end. La solución, aunque parece sencilla y adecuada para este caso puntual, no se verá así cuando se quiera extender la solución. Por ejemplo que la cantidad de candidatos varíe, digamos que en lugar de ser cinco, sean 15 candidatos, y que una vez modificado el programa se presente otro caso en el que hay sólo nueve. Al presentarse estas situaciones no nos quedaría otro camino que modificar el programa para que se adapte al caso y compilarlo nuevamente, y si presenta un nuevo caso volver a hacerlo. Un programa se debe plantear de modo que si las condiciones cambiaran en un futuro, sea el mismo programa el que se adapte a ellas y no tener que hacer modificaciones que impliquen modificaciones al código y re compilación de éste. Como veremos más 150
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
adelante esto se puede lograr, en algunos casos empleando tipos de datos estructurados.
Definición Un arreglo se define como una colección o conjunto de datos, que tiene como característica que todos los elementos que lo conforman son de un mismo tipo de dato, además todos se identifican por el mismo nombre. La manera de diferenciar un elemento de otro en el conjunto se hace por medio de uno o más índices. Dependiendo el número de índices por los cuales se identifique a los elementos de un arreglo se dirá que éste es un arreglo de una dimensión, de dos dimensiones, etc. A un arreglo de una dimensión se le reconoce también con el nombre de “vector”, y a uno de dos dimensiones como “matriz”. Implementación de arreglos unidimensionales La implementación o declaración de un arreglo en los diferentes lenguajes de programación no siempre es igual, sin embargo existen tres elementos en la declaración de un arreglo que aparecen en la mayoría de lenguajes. Primero aparece una indicación que le dice al compilador que la variable que estamos declarando es un arreglo y no un dato estándar, esta indicación es una palabra reservada (array) como lo hace el Pascal o un par de corchetes ( [ ] ) como en el lenguaje C. En segundo lugar se debe indicar el número de elementos del conjunto, en Pascal por ejemplo se emplea un rango de datos (1..10), en el caso de C sólo se indica la cantidad de elementos que tendrá (10). Finalmente se debe indicar el tipo de dato que podrá almacenar cada elemento, como ya se ha dicho, éste debe ser igual para todos. En el caso del Pascal, se recomienda que primero se defina un nuevo tipo de dato con la descripción del arreglo que se quiere emplear, y luego se emplee este tipo de dato para la definición de las variables que requiera el programa. Esto porque el Pascal no permite describir los tipos de datos en los encabezados de las funciones o procedimientos cuando se declara los argumentos. En el capítulo donde se estudia la estructura general de un programa se vio cómo se define un nuevo tipo de dato, según eso la declaración de un tipo de dato arreglo seguirá la siguiente sintaxis: type
Nombre del tipo
=
array
[
rango
]
of
tipo
;
De acuerdo a esta sintaxis, un tipo de dato para un arreglo se puede definir de la siguiente manera: type TipoVector = array [1..5] of Integer; Esta expresión define el tipo de dato denominado TipoVector el cual permitirá definir variables de tipo arreglo, con cinco elementos cada una, en el que cada elemento será de tipo Integer. Estas variables se podrán definir de la siguiente manera: var a, vector, lista: TipoVector; 151
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
El arreglo a, por ejemplo, define cinco variables ([1..5]) que se identificarán como a[1], a[2], a[3], a[4] y a[5], todas estas son de tipo Integer y podrán ser manipuladas como cualquier variable. Algo interesante en el manejo de arreglos es que el índice que diferencia a los elementos puede ser reemplazado por una variable cuyo valor esté dentro del rango definido para los índices del arreglo. En los siguientes ejemplos se muestra esta característica. program ejemploDeManejoDeArreglos; type TipoVector = array [1..5] of Integer; var a: TipoVector; i: Integer; begin i := 1; a[i] := 23; {al valer i = 1 se le asigna el valor 23 a la variable a[1]} i := i + 2; a[i] := 51; {al valer ahora i = 3 se le asigna 51 a la variable a[3]} for i := 1 to 5 do a[i] := i * i; { aquí el valor de i cambiará en cada ciclo y por lo tanto la asignación se hará a un elemento diferente del arreglo en cada ciclo, dependiendo del valor que tenga la variable i en el ciclo.} for i := 5 downto 1 do writeln('a[', i, '] = ', a[i]); { de igual manera, aquí la referencia al elemento del arreglo dependerá del valor de la variable i en cada ciclo.} end. Al ejecutar este programa podremos observar el siguiente resultado:
Algunos ejemplos de cómo definir un arreglo de una dimensión se muestran a continuación: type TipoVector2 = array [-5..10] of Char; var b: TipoVector2; Aquí se definen las variables b[-5], b[-4], b[-3], b[-2], b[-1], b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9] y b[10]. Todas de tipo Char. type TipoVector3 = array [‘a’..’j’] of Boolean; var c: TipoVector3; 152
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Definen las variables c[‘a’], c[‘b’], c[‘c’], c[‘d’], c[‘e’], c[’f’], c[‘g’], c[‘h’], c[‘i’] y c[‘j’]. De tipo Boolean. type TipoVector4 = array [false..true] of Real; var d: TipoVector4; Definen las variables d[false] y d[true]. De tipo Real. Otro elemento que se debe tomar en cuenta cuando se implementa un arreglo es que cuando uno define un arreglo, el tamaño o la cantidad de elementos del arreglo se deben definir en tiempo de compilación y no en tiempo de ejecución. Esto quiere decir que no se puede, durante la ejecución de un programa, que asignemos un valor a una variable (asignándola directamente o por lectura) y a partir de ella se defina un arreglo. No se puede hacer que un programa pida al usuario que ingrese el tamaño del arreglo y a partir de eso crear un arreglo de ese tamaño. La definición del arreglo, tanto en el tamaño como en el tipo de dato que va a manejar, debe estar definido antes que el programa se ejecute. Esto trae un problema que ilustro con un ejemplo. Volvamos al problema de la desviación estándar, queremos elaborar un programa que solucione el problema. Aquí se debe tomar en cuenta que el programa no se ejecutará una sola vez y que cada vez que se ejecute, la muestra que se emplee para el cálculo será diferente. No se puede suponer que siempre las muestras serán del mismo tamaño. Entonces, debido a que el tamaño del arreglo debe estar definido antes de ejecutar el programa y que no es lógico que el código del programa tenga que ser modificado en cada ejecución, las dudas que se presentan a este nivel serán ¿cuál es el tamaño que debe tener el arreglo para que pueda servir en todas las ejecuciones del programa? y ¿cómo manejar la cantidad de elementos en un arreglo que siempre tendrá el mismo tamaño? La primera incógnita debe manejarse analizando el ámbito en el que se ejecutará el programa, por ejemplo si la desviación estándar se va a aplicar sobre las evaluaciones de un curso, se debe examinar la fluctuación en el número de alumnos que ha tenido el curso, de repente un semestre tuvo 45 alumnos, otro 38, 65, 51 etc. otra cosa que podríamos averiguar es la capacidad máxima de las aulas en que se lleva a cabo el curso. En este sentido debe considerar que si decide definir el tamaño del arreglo con 65 elementos, es probable que en algún momento pueda tener más alumnos (p. e.: 69) y que por lo tanto el programa no se pueda ejecutar y haya que modificarlo y compilarlo nuevamente. Por otro lado si por asegurarnos damos al arreglo un tamaño como por ejemplo de 500, podemos estar ante una ineficiencia en el manejo de recursos, ya que nunca se llegará a ese número de alumnos en el curso. Hay entonces que buscar un equilibrio, quizás al ver que las aulas no pueden albergar más de 90 alumnos, ese sea el número que debemos manejar. Luego de haber determinado el tamaño del arreglo, otra cosa que se debe tomar en cuenta es que este tamaño debe quedar fijo y no se puede o debe cambiar. Por lo tanto, en los programas se deben definir adicionalmente una variable o variables que nos indiquen cuántos datos hemos colocado en el arreglo, de modo que no cometamos 153
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
el error de trabajar con el tamaño del arreglo en lugar de con el número de elementos que se han colocado en el arreglo.
Lectura e impresión de los datos en un arreglo unidimensional Para leer datos y asignarlos a un arreglo, se debe tener las mismas consideraciones que se tomaron en el capítulo de estructuras de control. Quiero decir con esto que se debe realizar esta operación pensando en el usuario, tratando que esta tarea sea rápida y sencilla, y que no tengamos que pedirle al usuario que introduzca datos más allá de los que son estrictamente necesarios. La impresión de los datos también debe hacerse de modo que los datos estén tabulados de manera que se puedan entender fácilmente. El siguiente ejemplo muestra una forma simple de ingresar datos a un arreglo desde la consola e imprimirlos luego: program leeArrConsola; const MAXALUM = 90; { Esta es la máxima cantidad de alumnos que se permite para el programa, se tomará como tamaño del arreglo}
type TArrNotas = array [1..MAXALUM] of Integer; var nota :TArrNotas; { Aquí se guardarán las notas. Las notas van de 0 a 20} numDatos: Integer; { Esta variable controlará el número de datos que se almacenará en el arreglo}
begin leerNotas(nota, numDatos); imprimeNotas(nota, numDatos); end. procedure leerNotas(var nota: TArrNotas; var numDatos: Integer); var aux: Integer; begin numDatos := 0; writeln('Ingrese las notas, para terminar ingrese una nota con valor -1:' ); repeat read(aux); if aux in [0..20] then begin inc(numDatos); nota[numDatos] := aux; end; until aux = -1; readln; end; procedure imprimeNotas(var nota: TArrNotas; numDatos: Integer); var n: Integer; begin for n:= 1 to numdatos do begin if (n-1) mod 5 = 0 then writeln; 154
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
write(nota[n]:5); end; writeln; end; En el programa anterior se pueden apreciar algunos puntos importantes: Se definió primero una constante llamada MAXALUM, con la finalidad que a partir de ella se declare el tipo de dato TArrNotas que defina al arreglo. Esto se hace con la finalidad que en una eventual situación en la que se deba modificar el tamaño del arreglo, sea más fácil de encontrar. Por otro lado si en alguna parte del programa se necesitara recorrer completamente el arreglo se use esta constante para definir el límite, de modo que si el tamaño cambiara, el cambiar el valor de la constante sea suficiente para que el recorrido se haga correctamente. La manera en que sea planteado la lectura de datos permita al usuario elegir la forma en que ingresará los datos, esto es, uno seguido del otro, uno por uno, en grupos, etc., debido a que se emplea una orden read que mantiene el buffer de entrada. A continuación se muestran formas diferentes en que el usuario puede ingresar los datos:
-
En cuanto a la impresión de los datos, ésta se hace tabulando los datos de modo que salgan manteniendo un orden que permita leerlos e interpretarlos sin dificultad. Sin embargo hay un detalle en este procedimiento que es importante aclarar, se trata de la forma como se manejan los parámetros. Se ha dicho que cuando uno pasa un parámetro por valor a un subprograma, el sistema toma el valor contenido en la variable o el resultado de la expresión y lo envía al subprograma, para esto se crea una nueva variable (argumento del subprograma) en donde se deposita el valor transmitido, cuando se termina de 155
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
ejecutar el subprograma esta variable se destruye. El paso por referencia no hace esta operación sino que se envía a subprograma la “referencia” (dirección de memoria) de la variable. En este sentido, cuando se pasa un arreglo como parámetro a un subprograma, si se hace por valor, el sistema tendría que crear un nuevo arreglo del mismo tamaño que el del parámetro, y luego asignar a cada uno de los elementos del arreglo, los valores enviados por el sistema. Este proceso es muy ineficiente, porque se duplica el uso de los recursos (se duplica la memoria empleada) y también retarda el proceso al tener que asignar todos los valores al nuevo arreglo. En este sentido se recomienda que cuando se pase un arreglo como parámetro a un subprograma, éste se haga por referencia, ya que ni se creará un nuevo arreglo, ni se asignarán nuevamente los valores del arreglo. La impresión de datos en el programa se diseñó en el ejemplo para que salga como a continuación se presenta:
Aplicaciones que emplean arreglos de una dimensión 1. El primer ejemplo que se presentará es el de la desviación estándar, en este programa se leerán los datos desde un archivo de textos. El programa calculará además el promedio del curso. Solución: program calculoDelaDesviasionEstandar; const MAX_DAT_ARR = 90; {Tamaño del arreglo} Type TipoNotas = array [1..MAX_DAT_ARR] of Integer; var nota: TipoNotas; numDat: Integer; prom, desv : Real; begin leedatos(nota, numDat); prom := promedio(nota, numDat); desv := desvEstandar(prom, nota, numDat); writeln('Promedio = ', prom:8:2); writeln('Desviasion estandar = ', desv :8:4); end. 156
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedure leedatos(var nota: TipoNotas; var numDat: Integer); var arch: Text; nombArch: String; begin write('Ingrese el nombre del archivo: '); readln(nombArch); assign(arch, nombArch); reset(arch); numDat := 0; { Con numDat se controlará el numero de datos y el índice del arreglo}
while not eof(arch) do begin inc(numDat); read(arch, nota[numDat]); end; close(arch); end; function promedio(var nota: TipoNotas; numDat: Integer): Real; var n: Integer; suma: Real; begin suma :=0; for n:=1 to numDat do suma := suma + nota[n]; promedio := suma /numDat; end; function desvEstandar( prom: Real; var nota: TipoNotas; numDat: Integer): Real; var n:Integer; suma : Real; begin suma :=0; for n:=1 to numDat do suma := suma + sqr(prom-nota[n]); desvEstandar := sqrt(suma/(numdat-1)); end; 2. En el siguiente ejemplo escribiremos un programa que permita determinar una distribución de frecuencias. Esta distribución de frecuencias se aplicará a una encuesta de intención de voto para una elección determinada. La distribución de frecuencias se mostrará mediante una gráfica de barras y para ver diferentes forma de manejar los arreglos se presentará primero como barras horizontales y luego como barras verticales. Partiremos de dos archivos de textos, uno contendrá los nombres de los candidatos ordenados por el número que lo identifica; el otro tendrá los datos obtenidos de la encuesta los cuales serán simplemente el número 157
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
del candidato por el que votaría el ciudadano. Los archivos podrán ser similares a los siguientes:
Solución: De acuerdo a los datos, el programa deberá leer los archivos de texto pero el proceso será diferente en ambos casos. Los nombres de los candidatos no serán procesados, sólo se requiere almacenarlos en memoria para luego tomarlos para imprimirlos; como se trata de varios nombres, la mejor manera de almacenarlos es un arreglo. En cuanto a los votos, la cantidad de encuestados pueden ser miles; no tiene sentido y sería un desperdicio de recursos guardar esos miles de datos en un arreglo, hay que pensar qué es lo que realmente se requiere almacenar. La distribución de frecuencias busca saber cuántas personas desean votar por un determinado candidato, por lo tanto el programa debe contar los votos de cada candidato. De acuerdo a esto se requiere un contador por cada candidato y por lo tanto se requiere otro arreglo en el programa que cumpla esta función. Finalmente se requiere calcular el porcentaje con respecto a total de encuestados que tiene cada candidato, esto nos obliga a definir un tercer arreglo en el programa. Por último, a pesar que los tres arreglos trabajarán con la misma cantidad de datos, guardarán diferentes tipos de información, por lo que se requiere definir tres tipos de datos diferentes. A continuación mostramos el programa principal que solucionará el problema. program distribucionDeFrecuencias; const MAX_CAND = 10; {Máximo número de candidatos} MAX_CAR = 50; {Constantes que limitan el área de impresión} MAX_LIN = 20; {de las gráficas} type TArrCand = array [1..10] of String; TArrVotos = array [1..10] of Integer; TArrPorc = array [1..10] of Real; var cand: TArrCand; votos: TArrVotos; porcent: TArrPorc; numCand: Integer; begin leerCandidatos (cand, numCand); contarVotos (votos, numCand); calcularPorcent (votos, numCand, porcent); imprimirHistogHor (cand, votos, porcent, numCand); writeln; writeln; imprimirHistogVert (cand, votos, porcent, numCand); end. 158
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Los procedimientos leerCandidatos y calcularPorcent son muy simples y no requieren mayor explicación: procedure leerCandidatos (var cand: TArrCand; var numCand: Integer); var archCand: Text; begin assign(archCand, 'Candidat.txt'); reset(archCand); numCand := 0; while not eof(archCand) do begin inc(numCand); readln(archCand, cand[numCand]); end; close(archCand); end; procedure calcularPorcent( var votos:TArrVotos; numCand:Integer; var porcent:TArrPorc); var suma, v :Integer; begin suma := 0; for v := 1 to numCand do suma := suma + votos[v ]; for v := 1 to numCand do porcent[v ] := votos[v ]/suma*100; end; En el procedimiento contarVotos se puede apreciar algunas de las ventajas del uso de los arreglos, aquí se ha considerado un arreglo en el que cada elemento llevará la cuenta de los votos de cada candidato. En este caso, a diferencia de lo que hicimos en el ejemplo al inicio de este capítulo, no se requiere que luego de leer la intención de voto de un elector tengamos que ejecutar una condicional por cada candidato para saber qué variable debemos incrementar. Aquí haremos que el índice de los elementos del arreglo coincida con el número del candidato, entonces luego de lee la intención de voto, ésta se usará como índice del elemento del arreglo que queremos incrementar. De este modo, sin importar cuántos candidatos haya, sólo se requiere una instrucción para realizar esta labor. procedure contarVotos (var votos: TArrVotos; numCand: Integer); var archVotos: Text; voto: Integer; begin {Inicializamos los contadores de votos} for voto:=1 to numCand do votos[voto] :=0; assign(archVotos, 'Eleccion.txt'); reset(archVotos); 159
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
while not eof(archVotos) do begin read(archVotos, voto); { Verificamos si el voto corresponde a un candidato e incrementamos los contadores, usando el voto como índice del arreglo}
if voto in [1..numCand] then inc(votos[voto]); end; close(archVotos); end; En los procedimientos que grafican los histogramas, como la salida será por la pantalla, deben contemplar algunos aspectos que tienen relación con la forma como se imprime en pantalla y el espacio que tenemos para escribir. En primer lugar, como se trata de un gráfico de barras, y en la ventana de salida sólo se pueden colocar caracteres, las barras se dibujarán empleando el caracter ‘ ▓’, cuyo código ASCII en el 178. Al colocar varios de estos caracteres seguidos se verá como una barra: ‘ ▓▓▓▓▓▓▓▓▓▓▓▓▓▓’. Luego, sólo 80 caracteres que podemos colocar en una línea de la pantalla, este valor no se puede modificar. Por lo tanto si queremos que en el histograma horizontal aparezca por cada candidato su nombre, el porcentaje que obtuvo, seguido por la barra que representa ese porcentaje, podemos ver que las barras tendrían una zona de aproximadamente 50 caracteres. Es por eso que se define en el programa principal una constante denominada MAX_CAR. Según esto último y en vista que estamos hablando de miles de encuestas, cada candidato podría acumular cientos o miles de votos. Esto nos hacer ver que no podemos imprimir un caracter ‘▓’ por cada voto, sino que cada caracter debe representar un conjunto de votos. El problema es que no podemos dar un valor fijo a este conjunto, porque la muestra de encuestados puede cambiar mucho. Por lo tanto, lo que se ha decidido es determinar el tamaño de ese conjunto de manera variable, en función de la muestra. Esto quiere decir que luego de calcular la cantidad de votos que tiene cada candidato, se determina quién tiene más votos, luego se establece que esa cantidad será graficada en el área total destinada para las barras. Las otras cantidades, que son menores, tendrán un tamaño proporcional al mayor, será cuestión de aplicar una regla de tres simple. El resto del código estará abocado a dar formato al reporte, que permita que éste se encuadre en la ventana. procedure imprimirHistogHor( var cand:TArrCand; var votos:TArrVotos; var porcent:TArrPorc; numCand:Integer); var max, v , c, numCar: Integer; begin max := votos[1]; {Determinamos la máxima cantidad de votos que recibió un candidato}
160
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
for v :=2 to numCand do if votos[v ]>max then begin max := votos[v ]; end; writeln(' Encuesta de opinion para las Elecciones'); writeln(' Histograma de Frecuencias Horizontal'); writeln; writeln('CANDIDATO':14,' % HISTOGRAMA'); writeln; for v := 1 to numCand do begin write(cand[v ]:14, porcent[v ]:10:2,'% | '); { Determinamos la cantidad de caracteres que se requieren para graficar la barra en proporción al máximo}
numCar := round(votos[v ] * MAX_CAR/max);
{ Procedemos a imprimir la barra caracter por caracter}
for c := 1 to numcar do write( #178); writeln; writeln; end; end; Para el caso del histograma vertical, aquí se presenta el problema que la impresión de caracteres en la pantalla se hace horizontalmente, línea por línea, de arriba hacia abajo, y esto no se puede cambiar. Por esta razón no se puede imprimir los resultados completos de un candidato y luego pasar al otro, como se hizo en el caso anterior. Lo que se tiene que hacer es imprimir franjas horizontales que abarque parte de todas las barras a la vez, como se indica en la figura siguiente:
Entonces, para imprimir una franja se debe verificar para cada candidato si corresponde o no imprimir parte de la barra, por ejemplo en la cuarta franja, como se ve a continuación, sólo se tiene que imprimir parte de la barras que corresponden a los candidatos 2 y 5,la de los otros no. Por lo tanto cuando se imprima esa franja, se deben imprimir espacios en blanco en aquellos candidatos cuya barra no llegue a la altura de la franja y los caracteres de la barra en aquellas que si lleguen. Veamos entonces el código. 161
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedure imprimirHistogVert(var cand:TArrCand; var votos:TArrVotos; var porcent:TArrPorc; numCand:Integer); var max, v , f, c, numCar: Integer; begin {Determinamos la máxima cantidad de votos que recibió un candidato}
max := votos[1]; for v :=2 to numCand do if votos[v ]>max then begin max := votos[v ]; end; writeln(' Histograma de Frecuencias Vertical'); writeln; for f := MAX_LIN downto 1 do begin {En cada ciclo del for se imprime una franja}
write('
');
{Imprimimos la parte de la barra correspondiente a cada candidato}
for v := 1 to numcand do begin if f <= round(votos[v ] * MAX_LIN /max) then {Determinamos si se debe o no imprimir la barra}
else end;
write(' ', #178, #178, #178, ' ') write('
');
{Al pie de las barras colocamos una leyenda con los candidatos}
if f <= numCand then write(numCand-f+1:3,') ', cand[numCand-f+1]); writeln; end; write(' '); {Imprimimos el número del candidato y su porcentaje}
for c:= 1 to numCand do write(c:7); writeln; write(' '); for c:= 1 to numCand do write(porcent[c]:6:2,'%'); writeln; end; El resultado final del programa se muestra a continuación:
162
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
3. El siguiente ejemplo muestra una forma muy sencilla de generar un “FRACTAL”. Se parte de un segmento de recta como se muestra en la figura 1. Teniendo las coordenadas de los extremos del segmento, se determina el punto medio del segmento y por allí se traza un segmento de recta “r” perpendicular al segmento original, de longitud aleatoria (figura 2). Luego se trazan los segmentos AP y PB, borrándose el segmento original (figura 3). Luego se toma cada uno de los nuevos segmentos y se les aplica a cada uno el mismo método, generándose 4 segmentos. Este proceso se repite muchas veces. Al final se obtienen formas similares las de la figura 4. Se desea determinar las coordenadas de los vértices de un “fractal” similar al que se muestra en la figura 4, por lo que se quiere confeccionar un programa en Pascal en el que se determine y almacene en dos arreglos dichas coordenadas, como se muestran en la figura 5. El programa leerá inicialmente las coordenadas de los extremos de un solo segmento de recta (coordenadas de A(X1, Y1) y B(X2, Y2)) y lo almacenara en las dos primeras celdas de los arreglos. Luego se leerá el número de veces que se desea se repita el método mencionado anteriormente. Este programa, en el primer ciclo, deberá determinar la coordenada del punto P e insertarlas en el arreglo (entre las dos primeras). Luego tomar de dos en dos las coordenadas y realizar lo mismo. Este proceso se repite tantas veces como se indicó. P P B(X2, Y2)
r
r m
m (Figura 2)
(Figura 1) A(X1, Y1)
B(X2, Y2)
B(X2, Y2)
A(X1, Y1)
(Figura 3) A(X1, Y1)
163
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo X1
Y1
X2
Y2
Xn
Yn
(Figura 5)
Solución: program calculaPuntosEnUnFractal; const MAX = 200; type TVector = array [1..MAX] of Real; var x, y: TVector; numPuntos, numCiclos, i: Integer; nombArch: String; arch: Text; begin randomize; write('Ingrese las coordenadas del primer punto: '); readln(x[1], y[1]); write('Ingrese las coordenadas del segundo punto: '); readln(x[2], y[2]); numPuntos := 2; {cantidad de puntos inicial en el arreglo} write('Ingrese el numero de ciclos: '); readln(numCiclos); write('Ingrese el nombre del archivo para los resultados: '); readln(nombArch); assign(arch, nombArch); rewrite(arch); {Definimos los puntos en cada ciclo y los insertamos en el arreglo}
for i := 1 to numCiclos do insertarPuntos(x, y, numPuntos); {Imprimimos los puntos obtenidos}
for i := 1 to numPuntos do writeln(arch, x[i]:8:3, y[i]:8:3); close(arch); end. En el procedimiento insertarPuntos vamos a tomar la pareja de puntos iniciales y calcularemos el punto P, luego lo insertaremos entre ellos, esto se hará en el primer ciclo. En el siguiente ciclo se tienen ahora tres puntos, aquí se tendrá que tomar una a una las parejas de puntos, comenzando desde el último, calcular el nuevo punto intermedio y luego insertarlo en el arreglo. El proceso se hace desde el último elemento del arreglo porque así se hace más fácil la inserción de los puntos. Esto debido a que el arreglo crecerá hacia abajo, 164
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
luego de insertar un punto, por lo tanto aquellos puntos que aun no se han procesado no se habrán movido de posición en el arreglo y por esto se encontrarán juntos a la hora de procesar la siguiente pareja. procedure insertarPuntos(var x, y: TVector; var numPuntos: Integer); var i: Integer; px, py: Real; begin { Tomamos una pareja de puntos en cada ciclo, comenzando desde el último, y calculamos el nuevo punto. Luego lo insertamos en el arreglo}
for i := numPuntos - 1 downto 1 do begin calcularPunto(x[i], y[i], x[i+1], y[i+1], px, py); insertarPunto(x, y, numPuntos, i, px, py); end; end; El procedimiento insertarPunto es simple, sólo hay que desplazar los puntos hasta la ubicar posición del que vamos a insertar. procedure insertarPunto( var x, y: TVector; var numPuntos, i: Integer; Px, Py: Real); var k: Integer; begin {Desplazamos los puntos del arreglo hasta ubicar la posición del nuevo punto}
for k := numPuntos downto i + 1 do begin x[k+1] := x[k]; y[k+1] := y[k]; end; x[i+1] := Px; y[i+1] := Py; inc(numPuntos); end;
En el procedimiento calcularPunto tenemos el siguiente esquema geométrico, para dos puntos cualesquiera: P (XP,YP)
B(X2, Y2)
r
a
β
a A(X1, Y1)
∆ y α
a =
AB
2
∆x
En consecuencia, el procedimiento se plantea de la siguiente manera: procedure calculaP(xi, yi, xi1, yi1: Real; var px, py: Real); var r, alpha, a, ap, betha: Real; begin r := random * 10; a := sqrt(sqr(yi1 - yi) + sqr(xi1 - xi))/2; ∆ y + ∆ x 2 ap := sqrt(a*a + r*r); 2
2
165
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
alpha:=arctan((yi1-yi)/(xi1-xi)); betha:=arctan(r/a); px:=ap*cos(alpha + betha) + xi; py:=ap*sin(alpha + betha) + yi; end; 4. Se desea hacer un programa en Pascal que permita reemplazar partes del contenido de un arreglo con otro. Para esto se cuenta con un archivo de textos con información similar al siguiente ejemplo: 24162424693596246529248557… 246 12345 113355 222 …
La primera línea del archivo consta de una serie de cifras, una a continuación de la otra, formando un número muy grande. No se sabe cuántos datos hay en esa línea pero en ningún caso habrán más de 300. Las siguientes líneas son similares a la primera. No se sabe cuántas líneas vienen después de la primera, pero sí que son un número par. El programa deberá leer la primera línea y almacenarla en un arreglo unidimensional. Luego leerá la segunda línea y lo almacenará en otro arreglo similar al anterior, finalmente lo hará con la tercera línea. Cuando los tres arreglos estén cargados, el programa deberá buscar la secuencia de cifras del segundo arreglo en el primero y reemplazarlas por la tercera, en todos los casos en que se produzcan. Una vez terminado el proceso deberá hacerse lo mismo con la cuarta y quinta línea, con la sexta y séptima, y así sucesivamente hasta terminar con el archivo. Finalmente deberá almacenar la secuencia de números que quedó en otro archivo de textos. Para el ejemplo anterior se tiene: Primer arreglo 2
4
1
6
2
4
2
4
6
9
3
5
9
6
2
4
6
5
2
9
2
4
8
5
5
7
…
Segundo arreglo 2
4
6
…
Tercer arreglo 1
2
3
4
5
…
Luego del primer proceso, el primer arreglo quedará de la siguiente manera: 2
4
1
6
2
4
1
2
3
4
5
9
3
5
9
6
1
2
3
4
5
5
2
9
2
4
8
5
5
7
…
Como se aprecia el primer arreglo podrá crecer o reducirse, dependiendo del los datos del archivo. Deberá tener en cuenta que si el arreglo está lleno o si la secuencia de reemplazo haría que se desborden los datos del arreglo no se deberá realizar el reemplazo. También deberá tener en cuenta que en el primer arreglo no deben quedar “huecos”. 166
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Solución: Este problema se presenta una dificultad, el tamaño de las secuencias a buscar y reemplazar. Estas pueden tener cualquier valor, esto quiere decir que ambas pueden ser del mismo tamaño o una puede ser más grande que la otra. En este sentido, si la solución se inclina a trabajar en función a los tamaños, tendríamos que definir bloques de instrucciones para cada caso, que podrían repetir código y que extendería demasiado el programa. Lo que hará la solución que se plantea aquí es que una vez encontrada la secuencia, ésta se eliminará del arreglo original desplazando los datos a la izquierda. Luego se desplazan los datos a la derecha de modo que quede un “hueco” del tamaño de la secuencia que va a reemplazar la que sale. Hacer esto elimina los casos, así ya no importará el tamaño relativo de una secuencia con respecto a la otra. La gráfica siguiente muestra ese proceso. Primer paso:
i ↓
2
4
1
6
2
4
2
4
6
9
3
5
9
6
2
4
3
5
9
6
2
4
6
5
…
9
3
5
9
6
↑
↑
↑
↑
↑
1
2
3
4
5
6
5
…
Se corren los datos a la izquierda
i Resultado : 2 4 1
↓
6
2
4
9
Se corren los datos a la derecha
i ↓
2
4
1
6
2
4
2
4
6
5
…
Se coloca la nueva secuencia en el hueco
El programa principal definirá tres arreglos, en el primero se guardará la secuencia original que irá siendo modificada en cada ciclo, los otros guardará la secuencia a buscar y la secuencia a reemplazar. A continuación presentamos el programa: program ReempSec; const MAX = 300; type TVectNum = array [1..MAX] of Byte; var archIn, archOut: Text; linea1, linea2, linea3: TVectNum; numDat1, numDat2, numDat3: Integer; begin preparaArchvos(archIn, archOut); leerSecuencia(archIn, linea1, numDat1); while not eof(archIn) do begin leerSecuencia(archIn, linea2, numDat2); leerSecuencia(archIn, linea3, numDat3); reemplazar(linea1,NumDat1, linea2, numDat2, linea3, numDat3); end; imprimir(archOut, linea1, numDat1); close(archIn); close(archOut); end. 167
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedure preparaArchvos(var archIn, archOut:Text); var nombArchIn, nombArchOut: String; begin write('Ingrese el nombre del archivo de datos: '); readln(nombArchIn); write('Ingrese el nombre del archivo de salida: '); readln(nombArchOut); assign(archIn, nombArchIn); reset(archIn); assign(archOut, nombArchOut); rewrite(archOut); end; procedure imprimir( var archOut: Text; var linea: TVectNum; numDat: Integer); var i:Integer; begin for i:= 1 to numDat do write(archOut, linea[i]); writeln(archOut); end; La lectura de una secuencia tiene que adaptarse a cómo están los datos en el archivo. Si revisamos éste, vemos que las cifras están una a continuación de la otra, sin espacios intermedios, esto nos llevará a leer las cifras de una manera peculiar. Primero debemos estar consientes que no se puede leer la secuencia como un número, esto porque la secuencia puede tener hasta 300 cifras, y el máximo número entero que podemos leer es el que puede ser representado en 4 bytes, esto es 4,294’967,294, por eso esta opción queda descartada. Otra limitación que tenemos es que por el hecho que las cifras no están separadas por espacios, no se pueden leer las cifras independientes, una por una, como números. Esto precisamente porque para leer una lista de números, estos deben estar separados por delimitadores que pueden ser espacios en blanco, tabuladores o cambios de línea. Lo que nos queda es leer las cifras caracter por caracter, teniendo que transformar el caracter leído en un número luego de su lectura, al final lo colocaremos en un arreglo. procedure leerSecuencia( var archIn:Text; var linea:TVectNum; var numDat:Integer); var c: Char; begin numDat:=0; while not eoln(archIn) do begin { Leemos un caracter a la vez de la secuencia y lo convertimos a número}
read(archIn, c); inc(numDat); linea[numDat] := ord(c)-ord('0');
168
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
end; readln(archIn); end; La operación de reemplazo de las secuencias recorrerá una a una las cifras de la secuencia original verificando en cada una si empieza allí la secuencia buscada, si la encuentra la elimina e inserta allí la nueva, esta operación es simple. Algo que sí debemos hacer en este proceso es tratar de reducir las ineficiencias que se pueden presentar. Primero podemos verificar que la posición de la cifra que estamos analizando permita encontrar la secuencia buscada. Nos referimos a que si la secuencia original tiene 20 cifras y estamos analizando el caracter 15, al margen de las cifras que estemos buscando, no podremos encontrar una secuencia que tenga más de seis caracteres. Entonces ya no tiene sentido continuar la búsqueda es ese caso. Otra cosa que debemos tener en cuenta es que no podemos colocar en la secuencia original más de 300 cifras. En este sentido, si luego de sacar una secuencia encontrada, la cantidad de cifras que debemos insertar hará que la secuencia original tenga más de 300 cifras, el proceso debe terminar. Según esto el procedimiento que se plantea es el siguiente: procedure reemplazar(var linea1: TVectNum; var numDat1: Integer; var linea2: TVectNum; numDat2: Integer; var linea3: TVectNum; numDat3: Integer); var i: Integer; {i: es el índice del arreglo, del elemento a analizar} begin i:=1; { Se verifica primero si hay suficientes cifras a partir de i para contener la secuencia a buscar. Luego se verifica si, luego de quitar la secuencia buscada, la secuencia que la reemplaza pueda entrar en el arreglo}
while (i <= numDat1 - numDat2 + 1) and (numDat1-numDat2+numDat3 <= MAX) do begin {Verificamos si en la posición i hay una secuencia}
if estaLaSecuencia(linea2, numDat2, linea1, i) then begin borrarSecuencia(linea1, numDat1, i, numDat2); insertarNuevaSecuencia(linea1, numDat1, linea3, numDat3, i); end; inc(i); end; end; A partir de aquí, los módulos siguientes se convierten en tareas muy simples de resolver. Los mostramos a continuación: function estaLaSecuencia( var linea2:TVectNum; numDat2:Integer; 169
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
var linea1:TVectNum; i:Integer):Boolean;
var k:Integer; sonIguales:Boolean; begin k:=1; sonIguales := True; while (k <= numDat2) and sonIguales do begin sonIguales := linea1[i] = linea2[k]; inc(i); inc(k); end; estaLaSecuencia := sonIguales; end; procedure borrarSecuencia(var linea1:TVectNum; var numDat1:Integer; i, numDat2:Integer); var k: Integer; begin { Corremos todos los datos a la izquierda eliminando la secuencia hallada}
for k:=i+numDat2 to numDat1 do begin linea1[i] := linea1[k]; inc(i); end; numDat1 := numDat1 - numdat2; end; procedure insertarNuevaSecuencia( var linea1:TVectNum; var numDat1:Integer; var linea3:TVectNum; numDat3, i:INteger); var k, m: Integer; begin
{ Corremos todos los datos a la derecha para hacer espacio a la secuencia de reemplazo}
m := numDat1 + numDat3; for k := numDat1 downto i do begin linea1[m] := Linea1[k]; dec(m); end;
{Se coloca la secuencia en el hueco formado}
for k:=1 to numDat3 do begin linea1[i] := linea3[k]; inc(i); end; numDat1 := numDat1 + numDat3; end;
170
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Ordenación de datos Ordenar datos es una tarea muy importante y frecuente en programación, además está relacionada directamente con el manejo de arreglos. Para ilustrar esto, veamos el siguiente ejemplo: queremos elaborar una aplicación que permita para un supermercado, dada una lista de compras de un cliente, determinar el total de la compra. La solución de este problema va por tomar uno a uno los artículos buscar en una tabla de artículos el productos, determinar el costo y agregar este monto al total. Pues bien, las búsquedas que hagamos en el proceso puede ser una tarea que tome la mayor parte del tiempo en la ejecución del programa, por lo que esta tarea debe realizarse con mucha eficiencia. Para entender esto veamos la siguiente analogía: si alguien quiere buscar el teléfono de una persona en la guía, a nadie se le ocurriría buscarlo desde la primera página, leyendo de uno por uno los nombres que allí aparezcan. Lo que uno hace es abrir la guía más o menos por la mitad, y luego dependiendo del nombre que buscamos, por ejemplo si buscamos a Rodríguez sabremos que lo encontraremos en la mitad de la derecha, pero si buscamos a Jiménez estará a la derecha. Luego repetimos la operación con la mitad correspondiente hasta ubicarlo. Esta manera tan rápida de encontrar un teléfono en la guía se puede hacer por una razón importante, los nombres de los usuarios están ordenados por el nombre de las personas. Imagínese que pasaría si en la guía que tenemos, los nombres de las personas estuvieran colocados conforme fueron adquiriendo su teléfono, ¿Cuánto tiempo nos tomaría encontrar el número de teléfono de una persona? Volviendo a la aplicación del supermercado, si los datos no estuvieran ordenados, por más rápida que puede ser la computadora, entregar el total de una lista de compras demoraría tanto que seguramente el supermercado perdería mucha clientela. Así como este ejemplo, la mayoría de aplicaciones a la que nos enfrentemos tendrán que realizar alguna búsqueda que, para que sea eficiente, deba trabajar con datos ordenados. En este capítulo veremos una forma simple de ordenar datos, sin embargo debemos mencionar que la ordenación de datos es una tarea que se ha estudiado por muchos años y se sigue estudiando, buscando una eficiencia muy alta en el proceso, por lo tanto existen innumerables métodos de ordenación de datos. El estudio de estos métodos y el análisis de su eficiencia son propios de un texto de Algoritmos y estructuras de datos, por lo que no profundizaremos mucho en este sentido. El método presentado a continuación se denomina “Método de intercambio”, el nombre se debe a la manera en que se ordenan los datos. Lo primero que haremos será explicar la forma cómo ordenaremos los datos para luego presentar el programa. Método de intercambio: Supongamos que tenemos la siguiente secuencia de datos que queremos ordenar: 47
53
35
19
97
59
77
12
93
171
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
El proceso irá tomando uno a uno los elementos el conjunto comparándolos con el primero, si vemos que el que analizamos es menor que el primero, los intercambiamos. De este modo al finalizar el recorrido habremos colocado el menor valor del conjunto en el primer elemento del arreglo. Veamos este proceso: 47
53
35
19
97
59
77
12
93 Aquí, 53 es mayor que 47: No hay intercambio
47
53
35
19
97
59
77
12
93 35 < 47: Los intercambiamos
35
53
47
19
97
59
77
12
93 19 < 35: Los intercambiamos
19
53
47
35
97
59
77
12
93 97 no es mayor que 19: No hay intercambio
19
53
47
35
97
59
77
12
93 59 no es mayor que 19: No hay intercambio
19
53
47
35
97
59
77
12
93 77 no es mayor que 19: No hay intercambio
19
53
47
35
97
59
77
12
93 12 < 19: Los intercambiamos
12
53
47
35
97
59
77
19
93 97 no es mayor que 19: No hay intercambio
12
53
47
35
97
59
77
19
93
No se intercambian
Se intercambian
Al final de este proceso vemos que el menor valor, el 12, terminó colocado en el primer lugar. Luego tomaremos nuevamente los datos, sin incluir el primero, y procederemos a realizar la misma tarea a partir del segundo elemento. Así colocaremos el segundo valor más pequeño en la segunda casilla: 12
19
53
47
97
59
77
35
93
El proceso continuará hasta haber ordenado todos los datos. 12
19
35
47
53
59
77
93
97
Implementación: El código del programa debe entonces definir un arreglo en donde se colocarán los datos que se quieren ordenar, luego el procedo de ordenación debe recorrer todos los elementos del arreglo y en cada elemento se deberá colocar el menor valor dentro de los que quedan a partir del elemento analizado. Tenemos que darnos cuenta que en este proceso, cuando lleguemos al penúltimo elemento, analizaremos ese elemento conjuntamente con el último, cuando esta tarea termine habremos colocado el menor en la penúltima casilla y el mayo en la última, por lo que el arreglo ya estará ordenado luego de terminar el proceso en la penúltima casilla, por lo tanto ya no se requiere otro ciclo para trabajar con el último elemento. El pseudocódigo para esta tarea sería como se muestra a continuación: Siendo n el número de elementos colocados en el arreglo arr: Para i = 1 hasta el penúltimo elemento (n-1) hacer Colocar en el elemento i del arreglo (arr[i]) el menor valor entre i y n
La tarea de colocar el menor valor en la casilla analizada (arr[i]) es otro proceso iterativo que va desde el elemento que sigue al que se está trabajando (arr[i+1]) hasta el último del arreglo. En cada ciclo se deberá tomar un elemento (arr[j]) y 172
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
compararlo con el elemento que se encuentra en la casilla que estamos trabajando (arr[i]); si el elemento de i es mayor que el elemento de j, estos valores se deben intercambiar de casillas. Así se conseguirá ordenar el arreglo de menor a mayor valor. El seudocódigo será el siguiente: Para j = i+1 hasta el último elemento (n) hacer Sí el valor de arr[i] es mayor que el de arr[j] entonces intercambiarlos
Según esto, el programa que permita ordenar un conjunto de datos numéricos de menor a mayor será el siguiente: program ordena const MAX = 100; type TVector = array [1..MAX] of Integer; var arr: TVector; numDat: Integer; begin leerDatos(arr, numDat); writeln('Datos en el orden original: '); imprimir(arr, numDat); ordenar(arr, numDat); writeln('Datos ordenados de menor a mayor: '); imprimir(arr, numDat); readln; end. procedure leerDatos(var arr: TVector; var numDat: Integer); var nombArch: String; arch: Text; begin write('Ingrese el nombre del archivo de datos: '); readln(nombArch); assign(arch, nombArch); reset(arch); numDat := 0; while not eof(arch) do begin inc(numdat); read(arch, arr[numDat]); end; readln(arch); close(arch); end; procedure ordenar(var arr: TVector; numDat: Integer); var i, j, aux: Integer; begin 173
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
for i := 1 to numDat -1 do for j := i+1 to numDat do if arr[i] > arr[ j] then begin {intercambiamos los valores}
aux := arr[i]; arr[i] := arr[ j]; arr[ j] := aux; end;
end; procedure imprimir(var arr: TVector; numDat: Integer); var i : Integer; begin for i := 1 to numDat do write(arr[i]:4); writeln; end; Al ejecutar el programa con un archivo de texto como se muestra a continuación:
Se obtendrá el siguiente resultado:
Adaptación del método de ordenación a problemas más complejos En este punto veremos cómo, empleando el mismo algoritmo de intercambio, podremos ordenar información que se presente de una manera más compleja. Por ejemplo, tenemos en un archivo la lista de empleados de una compañía, en la que se aprecie el código, nombre y sueldo de cada empleado y queremos ordenarla por el código del empleado. El archivo puede ser similar al siguiente:
Aquí, el programa deberá definir tres arreglos y en cada uno debe colocar los datos de los empleados, vale decir definir un arreglo de enteros para colocar los códigos, un arreglo de cadenas de caracteres para los nombres y otro de reales para los sueldos. El problema aquí es que si, como nos interesa ordenar los datos por código, ordenamos sólo el arreglo que contiene los códigos, obtendremos una repuesta equivocada, como veremos a continuación en el siguiente programa. 174
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
program ordenaCodigos_MAL; const MAX = 100; type TVectorInt = array [1..MAX] of Integer; TVectorStr = array [1..MAX] of String; TVectorReal = array [1..MAX] of Real; var codigo: TVectorInt; nombre: TVectorStr; sueldo: TVectorReal; numDat: Integer; begin leerDatos(codigo, nombre, sueldo, numDat); writeln('Datos en el orden original: '); imprimir(codigo, nombre, sueldo, numDat); ordenar(codigo, numDat); writeln('Datos ordenados de menor a mayor: '); imprimir(codigo, nombre, sueldo, numDat); readln; end. procedure ordenar(var codigo: TVectorInt; numDat: Integer); var i, j, aux: Integer; begin for i := 1 to numDat -1 do for j := i+1 to numDat do if codigo[i] > codigo[ j] then begin aux := codigo[i]; codigo[i] := codigo[ j]; codigo[ j] := aux; end; end; procedure leerDatos ( var codigo: TVectorInt; var nombre: TVectorStr; var sueldo: TVectorReal; var numDat: Integer); var nombArch: String; arch: Text; begin write('Ingrese el nombre del archivo de datos: '); readln(nombArch); assign(arch, nombArch); reset(arch); numDat := 0; while not eof(arch) do begin inc(numdat); readln(arch, codigo[numDat]); 175
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
readln(arch, nombre[numDat]); readln(arch, sueldo[numDat]); end; close(arch); end; procedure imprimir( var codigo: TVectorInt; var nombre: TVectorStr; var sueldo: TVectorReal; numDat: Integer); var i : Integer; begin for i := 1 to numDat do writeln(codigo[i]:9, nombre[i]:25, sueldo[i]:10:2); writeln; end; La respuesta que obtendremos con este programa será la siguiente:
Observe que los códigos están ordenados, pero estos códigos no coinciden con el correspondiente trabajador. Podemos ver en los datos originales que el código: 1111111 corresponde al trabajador: “Julia Gonzalez Cardenas”, pero en los datos ordenados parece corresponder a “Juan Perez Ramirez”. ¿Cómo podemos hacer para que esto se corrija? La respuesta está en el proceso de ordenación, lo que pasa es que al intercambiar los datos, se cambian de lugar los códigos, lo que provoca que se pierda la correlación con los otros datos (nombre y sueldo). Por lo tanto para que se siga manteniendo esta relación a la hora de intercambiar los códigos, se debe también intercambiar los nombres y sueldos. Entonces el algoritmo de ordenación se debe adaptar a esta situación, modificando el código del procedimiento ordenar como muestra a continuación: procedure ordenar( var codigo: TVectorInt; var nombre: TVectorStr; var sueldo: TVectorReal; numDat: Integer); 176
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
var i, j, auxCod: Integer; auxNom: String; auxSue: Real; begin for i := 1 to numDat -1 do for j := i+1 to numDat do if codigo[i] > codigo[ j] then begin auxCod := codigo[i]; codigo[i] := codigo[ j]; codigo[ j] := auxCod; auxNom := nombre[i]; nombre [i] := nombre [ j]; nombre [ j] := auxNom; auxSue := sueldo[i]; sueldo [i] := sueldo [ j]; sueldo [ j] := auxSue; end; end; Con esto se obtiene la siguiente respuesta, observe que la correlación de los datos se mantiene:
Un segundo caso que permite ver una situación compleja en la ordenación se da cuando pueden existir datos repetidos en el campo que se emplee como criterio de ordenación. Por ejemplo si tuviéramos un archivo en el que se encuentren los usuarios de la compañía de teléfonos, se tiene allí el nombre completo (nombre, apellido paterno y apellido materno) del usuario y su teléfono, como se muestra a continuación:
177
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Si ordenáramos estos datos por el apellido paterno, intercambiando todos los campos como en el problema anterior, podemos observar, como se muestra a continuación, que la respuesta del programa no es del todo satisfactoria:
Lo primero que se puede observar es que los datos se han agrupado por apellido paterno, también se puede ver que están ordenados por el mismo campo. Sin embargo hay algo que no está bien, si nos fijamos en un grupo de personas con el mismo apellido paterno, podemos darnos cuenta que allí no hay orden. Por ejemplo el usuario “Andres Caceres Juares” debería estar antes que “Guillermo Caceres Mora”, que “David Caceres Diaz” y “Cesar Caceres Diaz” deberían estar juntos y “Cesar” antes que “David”. Esto para lograr un orden como el que se encuentra en las guías telefónicas. Para lograr esto debemos analizar el algoritmo que empleamos para ordenar. Cuando el algoritmo hace la pregunta: if codigo[i] > codigo[ j] then begin, lo que se está haciendo es verificar si codigo[i] el está ordenado en relación con codigo[ j]. Por ejemplo, si estuviéramos ordenando de menor a mayor y en codigo[i] tuviéramos el valor 1111111 y en codigo[ j] el valor 5555555, no habría que intercambiarlos ya que 1111111 esta ordenado con respecto a 555555. Pero si en codigo[i] tuviéramos el valor 777777 y en codigo[ j] el valor 5555555, si se debería intercambiar porque no están ordenados relativamente. Entonces cuando los ordenemos alfabéticamente, empleado este criterio, debemos determinar cuándo dos datos deben intercambiarse. Si un usuario i fuera “Rocio Soria Tapia” y el usuario j fuera “Julio Diaz Cumpitaz”, sólo con comparar el apellido paterno sería suficiente para saber que debemos intercambiarlos, ya que “Diaz” debe estar antes que “Soria” y aquí no interesa cual es el apellido materno. 178
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Sin embargo, si el usuario i fuera “Rocio Soria Tapia” y el usuario j fuera “Victor Soria Castro”, no podemos tomar una decisión comparando sólo el apellido paterno ya que al ser iguales no se haría el intercambio y “Rocio Soria Tapia” quedaría antes que “Victor Soria Castro” y eso estaría mal. Entonces debemos pensar en una condición más compleja que abarque estas dos situaciones, la pregunta debería llevar a verificar que si no hay orden relativo en el apellido paterno se debe hacer el intercambio pero si ambos apellidos son iguales se debe intercambiar si no hay orden en el apellido materno. A esto se debe agregar que en el caso que ambos apellidos sean iguales, se deberá intercambiar si no hay orden en el nombre. Estos tres casos se deben expresar en una sola condición. El programa que resolverá este problema es el siguiente: program ordenaNombres; const MAX = 150; type TVectorInt = array [1..MAX] of Integer; TVectorStr = array [1..MAX] of String; var telefono: TVectorInt; nombre, apellPat, apellMat: TVectorStr; numDat: Integer; begin leerDatos(telefono, nombre, apellPat, apellMat, numDat); writeln('Datos en el orden original: '); imprimir(telefono, nombre, apellPat, apellMat, numDat); ordenar(telefono, nombre, apellPat, apellMat, numDat); writeln('Datos ordenados de menor a mayor: '); imprimir(telefono, nombre, apellPat, apellMat, numDat); readln; end. procedure ordenar( var telefono: TVectorInt; var nombre, apellPat, apellMat: TVectorStr; numDat: Integer); var i, j, auxTel: Integer; auxStr: String; begin for i := 1 to numDat -1 do for j := i+1 to numDat do if (apellPat[i] > apellPat[ j]) or ((apellPat[i] = apellPat[ j]) and (apellMat[i] > apellMat[ j])) or ((apellPat[i] = apellPat[ j]) and (apellMat[i] = apellMat[ j]) and (nombre[i] > nombre[ j])) then begin auxStr := nombre[i]; nombre[i] := nombre[ j]; nombre[ j] := auxStr; 179
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
auxStr := apellPat[i]; apellPat[i] := apellPat[ j]; apellPat[ j] := auxStr; auxStr := apellMat[i]; apellMat[i] := apellMat[ j]; apellMat[ j] := auxStr; auxTel := telefono[i]; telefono[i] := telefono[ j]; telefono[ j] := auxTel; end;
end; procedure leerDatos( var telefono: TVectorInt; var nombre, apellPat, apellMat: TVectorStr; var numDat: Integer); var nombArch: String; arch: Text; begin write('Ingrese el nombre del archivo de datos: '); readln(nombArch); assign(arch, nombArch); reset(arch); numDat := 0; while not eof(arch) do begin inc(numdat); readln(arch, nombre[numDat]); readln(arch, apellPat[numDat]); readln(arch, apellMat[numDat]); readln(arch, telefono[numDat]); end; close(arch); end; procedure imprimir( var telefono: TVectorInt; var nombre, apellPat, apellMat: TVectorStr; numDat: Integer); var i : Integer; begin for i := 1 to numDat do writeln(telefono[i]:9, nombre[i]:12, apellPat[i]:12,apellMat[i]:12); writeln; end; El resultado de este programa será el siguiente: 180
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Como se ve, es lo que esperábamos.
Cómo desordenar datos A pesar que esto parezca trivial, desordenar datos puede ser una tarea muy útil y compleja. Por ejemplo cuando se requiera una secuencia aleatoria de valores que no se repitan, esta situación se puede encontrar por ejemplo en la simulación de un juego de cartas, en donde la desordenación se refiere a barajar las cartas. Si piensa que esta tarea es fácil, y que sólo tiene que ejecutar una función aleatoria varias veces, verá que esto no es así. El siguiente programa mostrará cómo haciendo esto no se consigue lo deseado. program desordenar; {Imprime una secuencia de valores aleatorios entre 1 y 20}
var valor, i: Integer; begin randomize; writeln; for i := 1 to 20 do begin valor := random(20)+1; write(valor:3); end; writeln; readln; end. La respuesta a esto será:
Como se puede ver, por la misma naturaleza de la función aleatoria, se generará un número aleatoria en el rango pedido sin fijarse en los valores que generó anteriormente. Por lo tanto la lista contendrá valores repetidos. Entonces se podría pensar que para lograr una lista de datos desordenada en la que no se repitan los valores se tendría que generar el valor aleatorio y luego verificar si no 181
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
ha salido, si ya salió se deberá generar un nuevo valor. Sin embargo, como veremos a continuación esta no es una buena solución debido a que cuando salga un nuevo valor no repetido, aumentará la probabilidad que es siguiente se repita. Para conseguir el último valor se deberán generar muchísimos intentos. El siguiente programa muestra este algoritmo, y para ver la eficiencia hemos colocado un contador que nos dirá cuantas veces se ejecutará una instrucción crítica en el programa. program desordena2; const MAX = 100; type TArr = array [1..MAX] of Integer; var nCiclos: LongInt; {Variable global} var a: TArr; i: Integer; begin randomize; nCiclos := 0; for i := 1 to MAX do genera(a, i); for i := 1 to MAX do write(a[i]:4); writeln; writeln('Numero de ciclos: ', nCiclos); end. procedure genera(var a: TArr; i: Integer); var valor: Integer; begin repeat valor := random(MAX) + 1; until noEsta(a, i, valor); a[i] := valor; end; function noEsta(var a:TArr; i, valor: Integer): Boolean; var k: Integer; esta: Boolean; begin esta := false; k := 0; repeat inc(k); inc(nCiclos); {instrucciones que más se} esta := a[k] = valor; {repetirán en el programa} until (k = i) or esta; noEsta := not esta; end; El resultado es el siguiente: 182
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Como se puede observar el algoritmo obtiene el resultado esperado sin embargo el costo ha sido muy alto, 62,240 veces se ejecutó la instrucción más crítica. Este valor puede ser más grande aún, de acuerdo a la secuencia de valores que vayan saliendo. Esto no se puede aceptar. Ahora veremos un algoritmo muy ingenioso pero extremadamente eficiente que aplica el concepto de desordenar para generar una lista de valores sin repetir. El algoritmo empieza llenando un arreglo con valores consecutivos, y a partir de allí los desordena. Luego de esto, genera un valor aleatorio entre 1 y el tamaño del arreglo. El valor aleatorio se toma como una posición en el arreglo. Seguidamente se intercambia el último valor del arreglo con el de la posición obtenida con el valor aleatorio. Finalmente se repite el mismo proceso pero ahora se toman todos los datos del arreglo sin considerar el último. Debido al intercambio de valores, no importará que el valor aleatorio se repita ya que en esa posición no se encontrará el mismo valor. Este proceso se repite hasta que sólo quede un elemento por analizar. De esta manera se recorre una sola vez el arreglo y se conseguirá que la secuencia esté desordenada aleatoriamente. A continuación se presenta el programa: program desordenar; const MAX = 100; type TArr = array [1..MAX] of Integer; var a: TArr; i, pos, aux, limite, nCiclos: Integer; begin randomize; nCiclos := 0; {Llenamos el arreglo con valores consecutivos}
for i:= 1 to MAX do a[i]:= i; for limite:= max downto 2 do begin
{Determinamos un valor aleatorio entre uno y el límite}
pos := random(limite)+1; aux := a[limite]; {Intercambiamos los datos de esas posiciones} a[limite] := a[pos]; a[pos] := aux; inc(nCiclos); {Instrucciones críticas} end; {Imprimimos los valores}
for i:= 1 to MAX do write(a[i]:4); writeln; writeln('Numero de ciclos: ', nCiclos); end. 183
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
El resultado es el siguiente:
Observe que la cantidad de ciclos equivale a la cantidad de datos que se quiere generar y no como el algoritmo anterior. A continuación presentamos una pequeña aplicación en la que se aplica este concepto de desordenar. Se trata de un programa que permita repartir cartas a un grupo de jugadores. El programa leerá de un archivo los nombres de los jugadores y le repartirá a cada uno cinco cartas; previamente el programa barajará las cartas del mazo. Para simular el mazo de cartas se definirá un arreglo con 52 elementos, que corresponde al número de cartas de un mazo convencional. Cada carta será identificada por un número consecutivo empezando de cero, luego para el proceso de impresión de una carta, el número se relacionará con la carta de la siguiente manera: Las cartas que se encuentren entre 0 y 12 corresponderán a un palo de la baraja, las que se encuentren entre 13 y 25 a otro, entre 26 y 38 al tercero y de 39 a 51 al cuarto palo. De esta manera si recibimos un número cualquiera, bastará dividir el número entre 13 para obtener un valor 0, 1, 2 ó 3 que podemos relacionarlo con un palo. Casualmente en la tabla ASCII podemos encontrar las figuras correspondientes a las cartas en los caracteres de las posiciones 3 para los , 4 para los , 5 para los y 6 para las , con lo que sólo necesitaríamos sumarle 3 a nuestra división para tener el caracter ASCII correspondiente al palo. Finalmente el valor de la carta se obtiene del resto o módulo de la división. Así por ejemplo si recibimos el número 38, se tendrá que corresponde al 12 de , ya que 38 13 da como resultado 2 (2+3 = 5 ) con residuo 12. El programa se muestra a continuación: program desordenar; const NUMCAR = 52; MAXJUG = 10; Type TArrCartas = array [1..NUMCAR] of Integer; TArrStr = array [1..MAXJUG] of String; var mazo: TArrCartas; jugador: TArrStr; numJug: Integer; begin randomize; leeJugadores( jugador, numJug); barajar(mazo); repartir(mazo, jugador, numJug); end. 184
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedure barajar(var mazo: TArrCartas); var i, pos, aux, carta: Integer; begin for i := 1 to NUMCAR do mazo[i]:=i-1; for i := NUMCAR downto 2 do begin pos := random(i)+1; aux := mazo[i]; mazo[i] := mazo[pos]; mazo[pos] := aux; end; end; procedure repartir( var mazo: TArrCartas; var jugador: TArrStr; numJug: Integer); var carta, palo, j, c: Integer; begin for j:= 1 to NumJug do begin write( jugador[ j]:15, ': '); for c := 1 to 5 do begin carta := (mazo[( j-1)*5 + c] mod 13)+1; palo := mazo[( j-1)*5 + c] div 13; write(carta:5, chr(palo+3)); end; writeln; end; end; procedure leeJugadores( var jugador: TArrStr; var numJug: Integer); var nombArch: String; arch: Text; begin write('Ingrese el nombre del archivo con los jugadores: '); readln(nombArch); assign(arch, nombArch); reset(arch); numJug := 0; while not eof(arch) do begin inc(numJug); readln(arch, jugador[numJug]); end; close(arch); end; El resultado que obtendremos es el siguiente: 185
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
186
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 8: Arreglos de dos o más dimensiones En el capítulo anterior manejamos arreglos de una dimensión, es decir con variables que manejan un índice. Un arreglo de más de una dimensión se define de manera similar a la de un arreglo de una dimensión con la diferencia que cada elemento se distingue de otro por el uso de dos o más índices, manteniéndose la característica que todos los elementos manejan el mismo tipo de datos. Por ejemplo, una aplicación clara del uso de un arreglo de dos dimensiones es aquella en donde se necesite una tabla de doble entrada, otra aplicación se presenta en aplicaciones donde se requiera un algebra de matrices (suma de matrices, multiplicación de matrices, inversa de matrices, etc.).
Implementación de arreglos de más de una dimensión La sintaxis de la declaración de un arreglo de más de una dimensión varía ligeramente con respecto a los de una dimensión, a continuación se presenta el diagrama sintáctico: type
Nombre del tipo
=
rango
array
of
tipo
;
,
Al aplicar esta regla sintáctica un tipo de dato que permita definir un arreglo de dos dimensiones quedará de la siguiente manera: type TipoMatriz: array [1..10, 1..5] of Integer; var mat: TipoMatriz; La variable mat define los siguientes elementos: mat[1] mat[2] mat[3] mat[4] mat[5]
[1] [1] [1] [1] [1]
mat[1] mat[2] mat[3] mat[4] mat[5]
[1] [2] [2] [2] [2]
mat[1] mat[2] mat[3] mat[4] mat[5]
[3] [3] [3] [3] [3]
mat[1] mat[2] mat[3] mat[4] mat[5]
[4] [4] [4] [4] [4]
mat[1] mat[2] mat[3] mat[4] mat[5]
[5] [5] [5] [5] [5]
mat[1] mat[2] mat[3] mat[4] mat[5]
[6] [6] [6] [6] [6]
mat[1] mat[2] mat[3] mat[4] mat[5]
[7] [7] [7] [7] [7]
mat[1] mat[2] mat[3] mat[4] mat[5]
[8] [8] [8] [8] [8]
mat[1] mat[2] mat[3] mat[4] mat[5]
[8] [9] [9] [9] [9]
mat[1] mat[2] mat[3] mat[4] mat[5]
[10] [10] [10] [10] [10]
Al igual que con los arreglos de una dimensión, los índices se pueden manejar mediante expresiones, en esta caso cada índice es independiente del otro. Así se puede escribir una expresión como se indica a continuación: mat[i+3][k-1] := 87; La manera en que hemos presentado la definición de un arreglo de dos dimensiones es muy simple y fácil de realizar, sin embargo no es la manera más óptima de realizarla. Existen por lo menos dos razones para esto, la primera es que la forma como se ha definido el arreglo, a la hora que se le quiera pasar como parámetro a una función o procedimiento, sólo se le podrá pasar de dos maneras, o todo el arreglo o un solo elemento del arreglo. Esto es, en un programa se podrá escribir el siguiente código: x := func(mat); {se pasa el arreglo complete} proced(mat[i][ j]);{se pasa un elemento del arreglo} Los encabezados de estos módulos serán los siguientes: function func(var mat: TipoMatriz): Integer; {se recibe todo el arreglo} procedure proced(elemento: Integer); {se recibe un solo elemento} 187
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Si es cierto que estas formas son aceptables, no son suficientes. Para esto le pongo el siguiente ejemplo: supongamos que en un arreglo de dos dimensiones se encuentran almacenadas todas las notas de diferentes evaluaciones de los alumnos de un curso, si se quisiera calcular el promedio de un alumno, la función que permitirá este cálculo tendrá que recibir todo el arreglo, a pesar que sólo se quiere el promedio de un solo alumno. Esto lo hace ineficiente. La segunda razón para considerar que la manera en que definimos un arreglo bidimensional no es la mejor, es por la proyección que podemos hacer con lenguajes de programación más modernos. Se ha visto que esta manera de definirlas, permite modelar con una tabla de doble entrada como la que se aprecia en la figura siguiente: s a n m u l o C
1 Filas
2
3
4
5
6
7
1 2 3 4 5 6
Sin embargo en muchas aplicaciones se puede observar que el desperdicio de memoria en estas estructuras de datos es muy alto. Por esta razón los lenguajes de programación más modernos tienen una alternativa de definición de estos arreglos y es que primero se define un arreglo unidimensional que representan filas de un solo elemento y a cada uno de estos elementos se le asocia un arreglo, también unidimensional, representando las columnas. El esquema siguiente muestra esto: 1
2
3
4
5
6
7
1 2 3 4 5 6
Luego, al ser independientes cada fila, se puede controlar el tamaño de cada fila, independientemente una fila respecto a la otra, lo que reducirá el espacio desperdiciado, esto hace que el modelo en que podemos trabajar será el siguiente: s a n m u l o C
1 Filas
2
3
4
5
6
7
1 2 3 4 5 6
188
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Cabe indicar que a pesar de la forma, el manejo de los elementos es igual al de un arreglo bidimensional común, esto es mat[i][k]. Por estas razones es que vamos a cambiar, a partir de ahora, la forma en que vamos a definir los arreglos de dos dimensiones, de modo que podamos solucionar el problema de la primera razón y nos preparemos par que a la hora de migrar a otro lenguaje de programación no nos cueste mucho trabajo entenderla. Primero vamos a definir un tipo de dato que permita crear un arreglo unidimensional que modele una fila de nuestra estructura, este tipo se empleará para definir cada fila de nuestra estructura. Luego se definirá otro arreglo unidimensional con la cantidad de filas que requeriremos, finalmente le asignaremos a este arreglo el tipo de dato definido anteriormente. De este modo se estará definiendo un arreglo unidimensional en que cada elemento es un arreglo unidimensional. La forma de manejarlo luego en el programa será idéntica a la de la manera tradicional. Por lo tanto, la manera en que definiremos el arreglo inicial será como a continuación se detalla: Type TipoVector: array [1..5] of Integer; TipoMatriz: array [1..10] of TipoVector; var mat: TipoMatriz; Ahora, con esta definición además de poder llamar a una función o procedimiento pasando como parámetro todo el arreglo o un elemento del arreglo, podemos agregar el hecho de poder pasar una fila del arreglo, esto es: x := func(mat); {se pasa el arreglo complete} proced(mat[i][ j]); {se pasa un elemento del arreglo} y := func2(mat[i]); {se pasa una fila del arreglo} Los encabezados de estos módulos serán los siguientes: function func(var mat: TipoMatriz): Integer; {se recibe todo el arreglo} procedure proced(elemento: Integer); {se recibe un solo elemento} function func2(var fila: TipoVector): Integer; {se recibe una fila del arreglo}
Aplicaciones que emplean arreglos de dos o más dimensiones Algebra matricial El primer ejemplo que desarrollaremos es un programa que permita realizar un algebra vectorial en donde sumaremos y multiplicaremos matrices. El programa se planteará de la siguiente manera: program algebraDeMatrices; const MAX = 10; Type TVector = array [1..MAX] of Real; TMatriz = array [1..MAX] of TVector; var a, b, suma, mult: TMatriz; nFilA, nFilB, nFilS, nFilM, nColA, nColB, nColS, nColM: Integer; sumaOK, multOK: Boolean; begin 189
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
leeMat ('A', a, nFilA, nColA); leeMat ('B', b, nFilB, nColB); sumaOK := sumaMat(a, nFilA, nColA, b, nFilB, nColB, suma, nFilS, nColS); multOK := multMat(a, nFilA, nColA, b, nFilB, nColB, mult, nFilM, nColM); impMat ('A', a, nFilA, nColA); impMat ('B', b, nFilB, nColB); if sumaOK then impMat ('Suma', suma, nFilS, nColS) else writeln (‘No se pueden sumar’); if multOk then impMat ('Multiplicacion', mult, nFilM, nColM) else writeln (‘No se pueden multiplicar’); readln; end. El procedimiento leeMat, que permitirá leer los datos de una matriz y devolver también el numero de filas y columnas que manejara (p.e.: nFilA, nColA), se desarrollará tomando algunas consideraciones. En primer lugar se le agrega un parámetro al inicio, una cadena de caracteres que identifica a la matriz, esto para que a la hora de leer los datos se pueda identificar la matriz a la que le introducimos los datos. Luego, en cuanto a la lectura propiamente dicha, se debe tener en cuenta que cuando se trata de leer los datos de una matriz, determinar la cantidad de filas y columnas que tiene es una tarea muy sencilla para el usuario, a diferencia de cuando se tiene que leer una lista de datos que se introducirá en un arreglo lineal, la figura siguiente muestra este detalle. 2 15 7 6 9 14 3 − 1 21
18 43 7 11 ← Matriz de 3x 3 27 33 8 14 5 −6
16 25 2 21 71
25 19 5 ← Matriz de 5x 4 17 15
5 3 9 7 1 ← Matriz de 2x 5 6 8 2 4 8
Es por esta razón, que a diferencia de lo que se hace con la lectura de un arreglo lineal, aquí sí le pediremos al usuario que introduzca el numero de filas y columnas. En cuanto al ingreso de cada dato, haremos que el usuario los introduzca de una manera cómoda, en este sentido no lo obligaremos a introducir uno por uno los datos (indicando la fila y columna del dato que debe introducir), sino que los introduzca en conjunto por filas. A continuación se presenta este módulo: procedure leeMat (nomb: String; var m: TMatriz; var nFil, nCol: Integer); var f, c: Integer; begin {Se muestra la identificación de la matriz al solicitar las filas y columnas} write ('Ingrese el numero de filas y columnas de la matriz ' , nomb, ' : '); readln (nFil, nCol); for f := 1 to nFil do begin for c:= 1 to nCol do begin read (m[f][c]); {al usar read se permita leer varios datos por línea} end; readln; {limpia el buffer de entrada para leer sólo los datos de una fila} end; end; 190
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
El procedimiento que imprime una matriz se hará de modo que se muestre los elementos de la matriz tabulados de modo que se pueda apreciar fácilmente las filas y columnas de ella. El código es el siguiente: procedure impMat (nomb: String; var m: TMatriz; nFil, nCol: Integer); var f, c: Integer; begin writeln; writeln ('Matriz ',nomb, ': '); for f := 1 to nFil do begin for c := 1 to nCol do begin write (m[f][c]:8:2); end; writeln; end; end; Finalmente vienen los módulos que permitirán calcular la suma y multiplicación de las matrices, aquí se ha elegido emplear funciones porque de alguna manera debemos saber si se han podido operar, hay que recordar que para sumas dos matrices, ambas deben tener igual número de filas e igual número de columnas, y en el caso de la multiplicación, el número de columnas de la primera matriz debe ser igual que el número de filas de la segunda matriz. Si estas condiciones no se dan no se podrá sumar o multiplicar las matrices, por esta razón se decidió devolver un valor lógico que indique si se pudo o no operar las matrices. Las funciones devolverán también, a través de parámetros, las matrices de respuesta a las operaciones. Centrándonos en el proceso de la suma, el código del programa tendrá que basarse en el algoritmo matemático que se presenta a continuación: C = A + B: c1,1 c1,2 c2,1 c2,1 c m ,1 cm ,2
c1,n a1,1 c2,n a 2,1 = cm ,n a m ,1
a1,2 a2,1
a m ,2
a 1,n b1,1 b1,2 a 2,n b2,1 b2,1 + a m ,n bm ,1 bm ,2
b1,n b2,n bm ,n
Se define la suma de matrices como : ci , j = ai , j + bi , j , ∀i ∈ [1, m ], j ∈ [1, n ]
Según este algoritmo, para colocar en una matriz la suma de dos matices, primero debemos verificas que las dos matrices que se suman tienen igual número de filas e igual número de columnas. Luego se debe recorrer uno a uno los elementos de la matriz donde se colocará la respuesta y en cada elemento se colocará la suma del elemento, de cada matriz que se opera, que coincida con la posición del elemento recorrido. El recorrido de la matriz se hace de manera similar a la que se hace cuando se imprime la matriz. El código se muestra a continuación: 191
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
function sumaMat ( var a: TMatriz; nFilA, nColA: Integer; var b: TMatriz; nFilB, nColB: Integer; var s: TMatriz; var nFilS, nColS: Integer): Boolean; var f, c: Integer; begin if (nFilA = nFilB) and (nColA = nColB) then begin nFilS := nFilA; nColS := nColA; for f := 1 to nFilS do for c := 1 to nColS do s[f][c]:= a[f][c] + b[f][c]; sumaMat := true; end else begin sumaMat := false; end; end; Para el caso de la multiplicación, el algoritmo matemático en el que s tenga que basarse el código del programa se presenta a continuación: C = A×B: c1,1 c1,2 c2,1 c2,1 c m ,1 cm ,2
c1,n a1,1 c2,n a 2,1 = cm ,n a m ,1
a1,2 a2,1
a m ,2
a1, p b1,1 b1,2 a 2, p b2,1 b2,1 + a m , p bp ,1 bp ,2
b1,n b2,n bp ,n
Se define la multiplicación de matrices como : p
ci , j = ∑ ai ,k × bk , j , ∀i ∈ [1, m ], j ∈ [1, n ], k ∈ [1, p ] k =1
Según este algoritmo, para colocar en una matriz la multiplicación de dos matices, primero debemos verificas que el número de columnas del la primera matriz operando debe ser igual al número de filas de la segunda matriz operando. Luego se debe recorrer uno a uno los elementos de la matriz donde se colocará la respuesta y en cada elemento se colocará el producto escalar de dos vectores formados por, el primero, los elementos la fila de la primera matriz operando que coincide con la fila del elemento a calcular y el segundo, por el vector formado por los elementos de la columna de la segunda matriz que coinciden con coincidan con la columna del elemento a calcular. El recorrido de la matriz se hace de manera similar a la suma y el código se muestra a continuación: function multMat ( var a: TMatriz; nFilA, nColA: Integer; var b: TMatriz; nFilB, nColB: Integer; var m: TMatriz; var nFilM, nColM: Integer): Boolean; var f, c, k: Integer; begin 192
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
if nColA = nFilB then begin nFilM := nFilA; nColM := nColB; for f := 1 to nFilM do for c := 1 to nColM do begin m[f][c] := 0; for k := 1 to nColA do m[f][c] := m[f][c] + a[f][k]*b[k][c]; end; multMat := true; end else begin multMat := false; end; end; Al ejecutar el programa se obtendrá lo siguiente.
193
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Manejo de las filas de una matriz El segundo ejemplo que desarrollaremos permitirá manejar las filas de una matriz de manera independiente, de modo que se pueda pasar como parámetro a un procedimiento o función sólo una fila de la matriz, en lugar de pasar toda la matriz. El programa, que leerá de un archivo de textos el nombre y notas de los alumnos de un curso, imprimirá estos mismos datos pero ordenados por el promedio del alumno, promedio que será calculado en el programa para cada alumno. El archivo de datos será similar al siguiente: Juan Pérez Ruiz 10 14 7 12 19 11 12 14 9 12 17 12 Cecilia Rojas Ramos 11 12 4 15 12 13 10 7 12 17 8 10 Paula Castillo Sánchez 18 20 12 13 14 14 11 10 11 7 17 2 Alexandra Neyra Núñez
Todos los alumnos tienen igual cantidad de notas. 4 prácticas, 6 laboratorios, un examen parcial y uno final Promedio = 0.2xPP + 0.2xPL + 0.3xEXP + 0.3xExF Se elimina la menor nota de prácticas y laboratorios
…
Para resolver este problema se debe definir tres tipos de arreglos, uno que permita guardar los nombres de los alumnos, otro, de dos dimensiones, para guardar las notas, ya que también se imprimirán las notas parciales, y otro tipo para almacenar los promedios, los cuales serán valores reales a diferencia de las notas que serán valores enteros. El tipo de dato que manejará las notas se definirá en dos tiempos para así poder manejar las notas de un alumno, las que se encontrarán en una fila, de manera independiente. El programa se planteará de la siguiente manera: program promAlumn; const MAX_ALUM = 50; MAX_NOTAS = 12; type TVectNomb = array [1..MAX_ALUM] of String; TVectNotas = array[1..MAX_NOTAS] of Integer; TMatNotas = array [1..MAX_ALUM] of TVectNotas; TVectProm = array [1..MAX_ALUM] of Real; var alumno: TVectNomb; nota: TMatNotas; prom: TVectProm; numAl: Integer; begin leeDator (alumno, nota, numAl); calcularProm (nota, numAl, prom); ordenar (alumno, nota, prom, numAl); imprimir (alumno, nota, prom, numAl); end. Los procedimientos parar leer e imprimir los datos son simples por lo que los presentaremos de inmediato. procedure leeDator ( var alumno: TVectNOmb; var nota: TMatNotas; var numAl: Integer); var arch: Text; n: Integer; 194
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
begin numAl := 0; assign (arch, 'notas.txt'); reset (arch); while not eof(arch) do begin inc (numAl); readln (arch, alumno[numAl]); for n := 1 to MAX_NOTAS do read (arch, nota[numAl][n]); readln (arch); end; end; procedure imprimir ( var alumno: TVectNOmb; var nota: TMatNotas; var prom: TVectProm; var numAl: Integer); var a, n: Integer; begin for a := 1 to numAl do begin write (alumno[a]:30); for n := 1 to MAXNOT do write(nota[a][n]:3); writeln(prom[a]:6:1); end; end; El procedimiento que evaluará el promedio de cada alumno aprovechará la definición que hicimos del arreglo en dos partes. La función que calculará el promedio recibirá como parámetro una sola fila de la matriz. El código es el siguiente: procedure calcularProm ( var notas: TMatNotas; numAl: Integer; var prom: TVectProm); var n: Integer; begin for n:=1 to numAl do prom[n] := promedio(notas[n]); {pasamos solo una fila} end; La función promedio recibe la fila de la matriz, esto se puede hacer porque se ha definido el tipo de dato correspondiente a la fila (TVectNotas). function promedio (var notas: TVectNotas): Real; var n, suma, menor: Integer; prom, pP, pL: Real; begin suma := 0; menor := 21; {promedio de prácticas} for n := 1 to 4 do begin suma := suma + notas[n]; if notas[n] < menor then menor := notas[n]; end; 195
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
pP := (suma-menor)/3; suma := 0; menor := 21; {promedio de laboratorio} for n := 5 to 10 do begin suma := suma + notas[n]; if notas[n] < menor then menor := notas[n]; end; pL := (suma-menor)/5; prom := 0.2*pP + 0.2*pL + 0.3*notas[11] + 0.3*notas[12]; promedio := prom; end; El procedimiento que ordenará los datos, también aprovechará la definición en dos etapas de la matriz. A la hora de intercambiar los datos, el código se simplificará como se ve a continuación: procedure ordenar ( var alumno:TVectNOmb; var nota:TMatNotas; var prom: TVectProm; var numAl: INteger); var i, j: Integer; auxNomb: String; auxProm: Real; auxVect: TVectNotas; {definimos una fila auxiliar} begin for i := 1 to numAl-1 do for j := i+1 to numAl do if prom[i]
end; Al ejecutar el programa se obtendrá lo siguiente.
196
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Pupiletras En el tercer ejemplo vamos a crear un programa que permita ubicar palabras en una matriz, esta matriz almacenará letras de modo que simule el conocido juego denominado “pupiletras” o “sopa de letras”. Por ejemplo se quiere ubicar las palabras PASCAL, CATOLICA, ALUMNO, ENTERO, PLUMA, BYTE, WHILE en el siguiente tablero: 1 1 Q 2 P 3 L 4 J 5 O 6 B 7 K 8 B 9 H 10 X
2 3 W E K A A C H E X U Y T Y A Q E Y U C A
4 5 R T U V S A U C W I A L M O N T I A Z C
6 7 8 Y E U W G T P F A U L L Q F U S T M H J N E R O R S T A U C
9 I K Y I I M O D H V
10 P D R B O U A P R Y
El programa deberá buscar las palabras y emitir un reporte en el que se indique la coordenada de la primera letra de la palabra en el tablero, y la dirección en que se encuentra la palabra. El reporte será similar al siguiente: Palabra PASCAL CATOLICA ALUMNO ENTERO PLUMA PROGRAMA BYTE WHILE
Fila
Columna
Dirección
3 6 10 5 3 8 8 3 3 6 No se encontró 4 10 No se encontró
Hacia la Izquierda Hacia Arriba Hacia Abajo Hacia la Derecha Hacia Abajo-Derecha Hacia Arriba-Izquierda
Los datos se tomarán de dos archivos de texto, uno que contendrá las letras, y el otro las palabras. Lo primero que se debe hacer es definir cómo estarán estructurados los archivos, se debe buscar una forma sencilla de modo que la lectura de datos no sea una traba en el desarrollo del programa. En este sentido proponemos que el primer archivo sea similar al siguiente:
En el archivo se colocará en la primera línea el número de filas y columnas que tendrá el tablero, y a continuación, en las siguientes líneas se colocan las letras. Estas últimas se colocan en filas, tantas letras en una fila como columnas se hayan definido en el inicio del archivo. No se colocarán espacios entre las letras, ya que los espacios tendrían que ser leídos y no nos servirán luego; también porque en la lectura de caracteres no se requiere que éstos estén separados por espacios como si sucede con la lectura de números. 197
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
El archivo que contendrá las palabras es muy sencillo ya que se colocarán cada palabra en una línea independiente, como se muestra a continuación:
Lo que sigue ahora es plantear los tipos de datos que usaremos para modelar nuestros datos. Para las letras usaremos un arreglo de dos dimensiones de tipo Char, mientras que para las palabras un arreglo unidimensional también de tipo Char, en este último caso no se usará el tipo String porque vamos a tener que trabajar con cada uno de las letras que conforman la palabra y no con la palabra en conjunto; como aun no se ha desarrollado el tema de manejo de cadenas de caracteres, manejaremos las cadenas como arreglos. En este caso, como la matriz de caracteres que usaremos para las letras la definiremos en dos tiempos, emplearemos el tipo que define una fila de la matriz como tipo de dato para manejar las palabras. A continuación presentamos el programa: program pupiletras; const Max = 50; type TVector = array [1..Max] of Char; {TVector se empleará tanto en la matriz como en las palabras}
TMatriz = array [1..Max] of TVector; var letras: TMatriz; nFil, nCol: Integer; begin leerLetras(letras, nFil, nCol); imprimeLetras(letras, nFil, nCol); buscarPalabras(letras, nFil, nCol); end. La lectura de las letras es muy simple, por lo que la presentaremos en seguida: procedure leerLetras(var letras: TMatriz; var nFil, nCol: Integer); var nombArch: String; arch: Text; fil, col: Integer; begin write('Ingrese el nombre del archivo con el pupiletras: '); readln(nombArch); assign(arch, nombArch); reset(arch); readln(arch, nFil, nCol); for fil:=1 to nFil do begin for col:= 1 to nCol do read(arch, letras[fil][col]); readln(arch); end; end;
198
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
En el caso de la impresión de las letras leídas, es igual de sencilla, pero en este caso el código las mostrará centrada en la pantalla. procedure imprimeLetras (var letras: TMatriz; nFil, nCol: Integer); var fil, col: Integer; begin writeln; writeln('PUPILETRAS':45); {45 permite centrar el título en una ventana de 80 caracteres}
for fil:=1 to nFil do begin write(' ':39-nCol); {esto centra las letras} for col:= 1 to nCol do write(letras[fil][col]:2); writeln; end; writeln; end; El módulo que buscará las palabras se centrará en la lectura de las palabras del archivo de textos y su localización en el pupiletras. procedure buscarPalabras ( var letras: TMatriz; var nFil, nCol: Integer); var nombArch: String; arch: Text; palabra: TVector; {Se usa el tipo definido para la fila de la matriz} begin write('Ingrese el nombre del archivo con las palabras: '); readln(nombArch); assign(arch,nombArch); reset(arch); writeln; while not eof(arch) do begin leePalabra(arch, palabra); localizar(palabra, letras, nFil, nCol); end end; La lectura de la palabra se hará caracter por caracter del archivo, colocándose las letras en el arreglo. Aquí se presenta un problema, lo que pasa es que luego de colocar los caracteres en el arreglo, cuando queramos procesar la palabra, ¿cómo sabremos hasta dónde serán validos los caracteres en el arreglo?, podríamos hacer como en todos los casos en los que hasta ahora manejamos un arreglo de una dimensión, esto es definir una variable como numDat y allí colocar la cantidad de letras leídas, sin embargo emplearemos una forma diferente. El método que emplearemos en este problema es el que se utiliza en el lenguaje de programación C para manipular las cadenas de caracteres, esto es colocar al final de las letras leídas un caracter, que no se use con frecuencia, para delimitar los caracteres 199
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
válidos de los que no lo son. Este caracter será, al igual que en el lenguaje de programación C, el caracter cuyo código ASCII es cero ( #0). Esta forma de manejar las cadenas hará que los algoritmos que planteemos ya no se basen en llevar un contador que compararemos con la cantidad de caracteres que tenga el arreglo, si no que procesaremos cada caracter del arreglo hasta encontrar el caracter #0. procedure leePalabra (var arch: Text; var palabra: TVector); var i: Integer; begin i:=0; while not eoln(arch) do begin inc(i); read(arch, palabra[i]); end; palabra[i+1] := #0; {indica que se terminaron las letras leídas} readln(arch); end; El proceso de localizar una palabra en el pupiletras se hará en dos etapas, lo primero que debemos hacer es recorrer la matriz comparando el elemento de la matriz con la primera letra de la palabra, en el momento que se ubique una igualdad, empezaremos la segunda etapa, que consistirá en comparar la palabra con letras de la matriz en cada una de las direcciones en se puede encontrar, hacia la izquierda, derecha, hacia abajo, hacia arriba, diagonal hacia abajo y hacia la derecha, etc. El recorrido no se realizará mediante un doble for, esto porque debemos parar el proceso de búsqueda a penas encontremos una palabra en el pupiletras. Esto hará más eficiente el proceso ya que no tiene sentido seguir recorriendo la matriz luego de haber encontrado la palabra. procedure localizar(var palabra: TVector; var letras: TMatriz; nFil, nCol: Integer); var f, c: Integer; direccion: String; begin f := 1; direccion := 'NSE'; {indica: “No Se Encontró”} repeat c := 1; repeat if letras[f][c] = palabra[1] then direccion := buscaDireccion (palabra, letras, nFil, nCol, f, c); if direccion = 'NSE' then inc(c); until (direccion <> 'NSE') or (c > nCol); 200
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
if direccion = 'NSE' then inc(f); until (direccion <> 'NSE') or (f > nFil); imprime(palabra, f, c, direccion); end; La función que busca la dirección de la palabra, caracter por caracter, deberá hacerlo en las ocho direcciones posibles, esto es: arriba, abajo, izquierda, derecha, diagonal hacia la derecha arriba y abajo, y diagonal derecha arriba y abajo. Para buscar la palabra en una dirección dada, fijaremos primero una dirección, por ejemplo derecha, según esto deberemos tomar uno a uno los caracteres de la matriz que se encuentran a la derecha del elemento que coincide con la primera letra de la palabra buscada y compararlos con los de la palabra. Teniendo la fila y columna del elemento, para hacer esta tarea sólo tendremos que ir incrementado la columna. Si la dirección elegida fuera a la izquierda, el proceso sería similar al anterior pero deberíamos ir decrementando la columna. En el caso de elegir una dirección diagonal, por ejemplo derecha-arriba, el proceso deberá ir incrementando la columna y decrementando la fila. El siguiente gráfico muestra todas estas alternativas o direcciones, el incremento o decremento se indica con un 1 ó -1 respectivamente en las columnas (DH-dirección horizontal) o filas (DV-dirección vertical). DH=-1 DV=-1
DH= 0 DV= 1
DH=-1 DV= 0 DH=-1 DV= 1
DH= 1 DV=-1 DH= 1 DV= 0
DH= 0 DV= 1
DH= 1 DV= 1
Si nos fijamos un poco en los valores dados en la gráfica, veremos que DH toma los valores de -1, 0 y 1, y que DV también toma los valores -1, 0 y 1. De acuerdo a esto, el módulo que busca la dirección en que puede estar la palabra, sólo debería definir dos variables (dh y dv) y mediante un doble for generar los valores que permitan definir la dirección a buscar. Una vez fijados estos valores la nueva celda a analizar se determinará sumando estos valores a fila y columna de la celda inicial. Esto se aprecia en el código siguiente: for dv := -1 to 1 do for dh := -1 to 1 do if (dv <> 0) or (dh <> 0) then begin f := f + dv ; c := c + dh; … {El if es para descartar la dirección (0,0) que harían que f y c no cambien.}
Ahora, como lo que queremos es un proceso rápido, debemos parar las iteraciones a penas encontremos la palabra en una dirección, por eso en el código de la función no se usará for si no un ciclo con salida controlada. El código es el siguiente: 201
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
function buscaDireccion (var palabra: TVector; var letras: TMatriz; nFil, Ncol, f, c: Integer): String; var dv , dh: Integer; direccion: String; begin direccion:= 'NSE'; dv := -1; repeat dh := -1; repeat if (dv <> 0) or (dh <> 0) then direccion := verificaDireccion (palabra, letras, nFil, nCol, f, c, dv , dh); inc(dh); until (dh > 1) or (direccion <> 'NSE'); inc(dv ); until (dv > 1) or (direccion <> 'NSE'); buscaDireccion := direccion; end; Una vez que se fija la dirección en que se va a buscar la palabra ( dh y dv ), se debe recorrer la matriz en esa dirección comparando las letras de las celdas de la matriz con las letras de la palabra. Como ya hemos indicado, esta tarea consiste sólo en ir sumando los valores de dh y dv a la fila y columna inicial. Este recorrido debe terminar de manera eficiente, esto quiere decir que no debemos recorrer necesariamente toda la palabra para saber si la palabra coincide o no con los caracteres de la matriz en la dirección dada. Si al avanzar en la matriz nos damos cuenta que nos salimos de los límites de la matriz, el proceso debe terminar en ese instante, sabiendo que no está la palabra en esa dirección. Si un carácter de la matriz no coincide con el de la palabra, también el proceso debe terminar. Por último, si llegamos al carácter #0 habremos corroborado que todos los caracteres de la palabra se encuentran en la matriz en la dirección dada. La función verificaDireccion realiza esta tarea. function verificaDireccion ( var palabra: TVector; var letras: TMatriz; nFil, Ncol, fil, col, dv , dh: Integer): String; var k: Integer; begin k := 1; repeat inc(k); fil := fil + dv ; col := col + dh; until not (fil in [1..nfil]) or not (col in [1..ncol] ) or (palabra[k] = #0) or (letras[fil][col] <> palabra[k]); if palabra[k] = #0 then verificaDireccion := strDireccion (dv , dh) 202
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
else
verificaDireccion := 'NSE';
end; En este punto sabremos que hemos o no encontrado la palabra a partir de una celda y en una dirección dada. Sin embargo, la dirección la tenemos definida como un par de valores entre -1 y 1. Necesitamos traducir ese código numérico a una frase que el usuario entienda, por eso la función strDirección realizará esa tarea: function strDireccion (dv , dh: Integer): String; var dir: String; begin case dv of -1: begin case dh of -1: dir := 'Hacia Arriba - Izquierda'; 0: dir := 'Hacia Arriba'; 1: dir := 'Hacia Arriba- Derecha'; end; end; 0: begin case dh of -1: dir := 'Hacia la Izquierda'; 1: dir := 'Hacia la Derecha'; end; end; 1: begin case dh of -1: dir := 'Hacia Abajo - Izquierda'; 0: dir := 'Hacia Abajo'; 1: dir := 'Hacia Abajo - Derecha'; end; end; end; strDireccion:= dir; end; Finalmente, teniendo ya toda la información, la podemos mostrar en la pantalla. Tsta tarea la realiza el módulo imprime: procedure imprime ( var palabra: TVector; f, c: Integer; dir: String); var i, j: Integer; begin write ('Palabra: '); i := 1; while palabra[i] <> #0 do begin write (palabra[i]); inc(i); end; 203
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
for j := 1 to 15-i do write(' '); if dir = 'NSE' then writeln(' No se encontro') else writeln(' Posicion: ', f:2,' , ', c:2,' ', dir) ; end; La ejecución de este programa dará la siguiente respuesta:
El Juego de la vida Una bonita aplicación que maneje arreglos de más de una dimensión es la implementación de lo que se denomina “el juego de la vida”, este juego, fue diseñado por los años 70 por un matemático británico llamado John Horton Conway, de lo que se trata es de simular la evolución de organismos o células en un medio. Esta simulación permite mostrar una serie de patrones muy interesantes que han sido estudiados por mucho tiempo. El medio se simula mediante un tablero con una serie de casillas, cada casilla puede estar vacía o contener un "organismo". Un segmento del tablero puede ser como se muestra a continuación:
La siguiente generación de organismos se determina de acuerdo a reglas muy sencillas que a continuación se detallan: a. Nacimiento: Un organismo nacerá en cada celda vacía del tablero que tenga exactamente tres vecinas. b. Muerte: Un organismo que tenga cuatro o más organismos como vecinos morirá por apiñamiento. Un organismo con menos de dos vecinos morirá de soledad. c. Supervivencia: Un organismo con dos o tres vecinos sobrevivirá hasta la siguiente generación. Un organismo es influenciado sólo por los organismos de su generación, no puede ser influenciado por los organismos que aparecerán o desaparecerán en la próxima generación. A continuación se muestran cómo quedarían los organismos en dos generaciones siguientes al ejemplo anterior: 204
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Generación 2
Generación 3
El programa que desarrollaremos leerá de un archivo de textos la distribución de una generación de organismos, luego se solicitará al usuario el número de generaciones que desea se formen a partir de la inicial. Finalmente el programa deberá mostrar en pantalla cada una de las generaciones. En la solución al problema, primero definiremos la estructura de datos que manejará el medio. Como los organismos pueden estar en dos estados únicamente, esto es, pueden estar vivos o pueden estar muertos, hemos pensado en una matriz de valores lógicos (boolean) en dónde true significará que el organismo está vivo y false que está muerto. Sin embargo, habrá que tener en cuenta que el archivo de textos que se use para el ingreso de los datos no podrá tener esos valores, ya que en Pascal no se pueden leer datos de tipo boolean. Por eso es que decidimos que el archivo de textos sea de caracteres, la lectura será muy simple, y transformar el caracter leído en un valor lógico también es algo sencillo, y para hacerlo más sencillo aun definiremos en el archivo como un organismo vivo al caracter 0 (cero) y cualquier otro caracter significará un organismo muerto. Otra cosa que debemos definir es la estrategia con que vamos a atacar el problema. La solución debe orientarse a recorrer el arreglo y en cada celda se deberá calcular la cantidad de vecinos vivos que haya, luego se determinará si en la celda nacerá un organismo, si sobrevivirá morirá el que está allí. Aquí se presenta un problema, si tomamos a la ligera la solución, el programa crecerá en forma desmedida, nos referimos a que en todas las celdas no se pueden contar los vecinos de la misma manera, si vemos la figura siguiente, veremos en un celda del interior de la matriz pueden haber hasta 8 vecinos, pero en el borde sólo pueden haber un máximo de cinco, además es muy diferente contar las celdas vecinas del borde superior que del borde lateral o inferior. Por último están las esquinas que sólo pueden tener hasta 3 vecinos.
Observe que se trata de nueve casos diferentes, por lo que tendríamos que escribir código para cada uno haciendo que éste se extienda mucho. Sin embargo, como hemos dicho, tenemos que plantear una estrategia que elimine los casos y deje una sola alternativa. ¿Cómo se hará esto? Pues bien, haciendo que todas las 205
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
celdas tengan la misma cantidad máxima de vecinos. Esto se conseguirá definiendo la matriz de una manera particular; dándole al tipo de dato una fila más al inicio (fila 0) y una fila más al final (fila n+1) del que se pretendía dar inicialmente (n). De la misma manera se le dará una columna más al inicio (columna 0) y una al final (columna n+1). Luego, cuando se lean los datos se forzará que éstos no sean colocados en estas filas y columnas adicionales, quedándose éstas con organismos muertos. Cuando se recorra la matriz no se tomarán estas caldas, sin embargo a la hora de contar los vecinos sí, pero como los organismos en estas celdas estarán muertos, no alterará la cuenta pero si eliminará los casos. Observe los casos en la siguiente figura: columna n+1
columna 0 fila 0
fila n+1
Se puede apreciar que cualquier celda que analicemos se encuentra bajo las mismas condiciones, a la hora de contar los vecinos, se analizará siempre 8 celdas, con la salvedad que en las celdas de los bordes no hay organismos. El archivo de datos debe permitir colocar la información de los organismos de un medio de modo que sea lo más sencillo posible para el usuario, como ya hemos indicado estará compuesto por caracteres, de los cuales el carácter ‘0’ será considerado como un organismo vivo y cualquier otro uno muerto. En el archivo no se colocará la cantidad de filas y columnas que tendrá el medio que queremos simular, como hemos hecho hasta ahora en los anteriores ejemplos, esto porque queremos que el usuario se dedique sólo a “dibujar” el medio; imagínese lo tedioso que sería que luego de “dibujarlo” tenga que contar la cantidad de filas y columnas que colocó. Por eso el programa será el que cuente o calcule estos datos. Un ejemplo del archivo de datos que se puede emplear será el siguiente:
Observe que hemos empleado puntos para indicar los organismos muertos, también vea lo complicado que sería contar las filas y columnas luego de haber colocado los caracteres 206
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
en el archivo, o habiendo definido el numero de filas y columnas que queremos usar, lo difícil que sería colocar los caracteres de modo de coincidir con esos valores. Según lo planteado hasta ahora, el programa se diseñará de la siguiente manera: program juegoDeLaVida; uses crt; const MAX = 50; type TVector = array [0..MAX+1] of Boolean; TMatriz = array [0..MAX+1] of TVector; var organismo: TMatriz; nFil, nCol, numGen, ng: Integer; begin inicializaMatriz(organismo); leerGeneracion(organismo, nFil, nCol); write('Ingrese el numero de generaciones que quiere formar: '); readln(numGen); for ng := 0 to numGen do begin mostrarGeneracion(organismo, nFil, nCol); nuevaGeneracion(organismo, nFil, nCol); writeln('Generacion ', ng); writeln('Presione el ENTER para ver la siguiente generacion'); readln; clrscr; //borramos la pantalla, se define en crt end; end. Observe el uso de la biblioteca de funciones crt, en Pascal estas bibliotecas se llaman unidades (unit). Esto se realiza porque no queremos que la pantalla se llene con un listado interminable con todas las generaciones formadas. El programa mostrará una generación y luego pedirá al usuario que presione la tecla ENTER para ver la siguiente generación, en ese instante el programa borrará la pantalla para que aparezca la nueva generación en el mismo lugar que la anterior. Para borrar la pantalla usamos el procedimiento clrscr (clear screen). Cabe mencionar que en la biblioteca crt se encuentran todas las funciones y procedimientos que permiten controlar la pantalla o ventana de salida de datos. El programa primero inicializará la matriz para garantizar que todas las celdas, incluso la de los bordes tengan organismos muertos. procedure inicializaMatriz(var mat: TMatriz); var f, c: Integer; begin for f:= 0 to MAX+1 do for c := 0 to MAX+1 do mat[f][c] := false; end; El procedimiento leerGeneracion tendrá la tarea, como hemos indicado anteriormente, de leer los caracteres, convertirlos en un valor lógico y asignarlos a la matriz. Adicionalmente deberá contar el número de filas y columnas que manejará la matriz: 207
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedure leerGeneracion ( var organismo: TMatriz; var nFil, nCol: Integer); var nombArch: String; arch: Text; f, c: Integer; valor: Char; begin write('Ingrese el nombre del archivo con la 1ra. generacion: '); readln(nombArch); assign(arch, nombArch); reset(arch); nFil := 0; nCol := 0; f := 0; while not eof(arch)do begin inc(f); inc(nFil); //Aquí contamos las filas de la matriz c:=0; while not eoln(arch) do begin if f = 1 then inc(nCol); //Aquí contamos las columnas inc(c); read(arch, valor); organismo[f][c] := valor = '0'; // si valor es igual a ‘0’ se asigna true, de lo contrario false
end; readln(arch); end; close(arch); end; El procedimiento que imprime las generaciones (mostrarGeneracion) es muy sencillo, sin embargo aquí debemos tener en cuenta que debemos transformar los valores lógicos de la matriz en caracteres. En este caso se colocará un espacio en blanco para los valores falso y el caracter (ASCII 1) para los valores verdadero. Lo mostramos a continuación: procedure mostrarGeneracion ( var organismo: TMatriz; nFil, nCol: Integer); var f, c: Integer; begin for f := 1 to nFil do begin for c := 1 to nCol do write(chr( ord(organismo[f][c]) + ord(' ')*ord(not organismo[f][c]))); // #1 (1) true, ‘ ‘ false
writeln; end; end; Para crear la nueva generación debemos tener en cuenta que los organismos se crean, se mantienen vivos o mueren en función de los organismos que existe en la actual generación, y no se ven influenciados por la nueva generación. Por lo que la nueva generación se debe determinar en una matriz auxiliar y no en la misma en la que está la 208
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
actual. Una vez concluida la generación se copiará íntegramente en la matriz original. Veamos esto: procedure nuevaGeneracion ( var organismo: TMatriz; nFil, nCol: Integer); var orgAux: TMatriz; numVecinos, f, c: Integer; begin inicializaMatriz(orgAux); for f := 1 to nFil do begin for c := 1 to nCol do begin numVecinos := cuentaVecinos(f, c, organismo); if not organismo[f][c] then if numVecinos = 3 then orgAux[f][c] := true else orgAux[f][c] := false else if numVecinos in [2, 3] then orgAux[f][c] := true else orgAux[f][c] := false; end; end; organismo := orgAux; end; Finalmente la cuenta de vecinos se hará teniendo en cuenta el mismo criterio empleado en el “pupiletras” para tomar los caracteres en las diferentes direcciones. Aquí hay que aclarar que en el código no se tomará en cuenta la verificación que se debe hacer para evitar salirnos de los límites de la matriz ya que la matriz ha sido definida con filas adicionales en los bordes. function cuentaVecinos ( f, c: Integer; var organismo: TMatriz): Integer; var cont, i, j: Integer; begin cont := 0; for i := -1 to 1 do for j := -1 to 1 do if (i <> 0) or ( j <> 0) then cont := cont + ord(organismo[f+i][c+ j]); cuentaVecinos := cont; end; La ejecución del programa se verá como a continuación se presenta:
209
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
210
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 9: Manejo de cadenas de caracteres Definición Una cadena de caracteres es un conjunto de caracteres que se almacenan en un arreglo unidimensional, los diferentes lenguajes de programación proveen una serie de funciones y procedimientos que permiten manejar textos en un programa de manera sencilla a través de las cadenas de caracteres. A pesar que las cadenas de caracteres son simplemente arreglos, se requieren otros elementos para poderlos manejar, esto sencillamente porque si por ejemplo definimos un arreglo de 20 elementos y allí colocamos los caracteres que formen la palabra ‘Juan Pérez’, podemos darnos cuenta que colocar los caracteres es una tarea sencilla, pero a la hora de querer recuperarlos, por ejemplo imprimirla, ¿cómo lo hacemos? La frase ‘Juan Pérez’ no tiene 20 caracteres, sólo tiene 10, entonces, ¿cómo hacemos para imprimir sólo 10 caracteres y no los 20 con los que fue definido el arreglo?. Uno podría decir que ya que estamos trabajando con arreglos, por qué no definimos una variable auxiliar para manejar su tamaño como se hace con los arreglos comunes, no obstante esto se hace poco práctico, sobre todo cuando estemos hablando de varias cadenas de caracteres, como por ejemplo manejar un conjunto de nombres para ordenarlos, en este caso se tendría que definir un arreglo auxiliar de enteros para manejar las longitudes de cada nombre. Por esta razón muchos lenguajes de programación definen reglas y modos para declarar un variable como cadena de caracteres para que su manejo sea más práctico. Analicemos a continuación algunos de estos casos. El lenguaje Pascal, como se ha visto en los capítulos anteriores, una cadena de caracteres se define mediante la palabra String, esto define un arreglo de 256 elementos, cuyos índices van del 0 al 255, cuando se hace una asignación a la variable, internamente el sistema coloca los caracteres a partir del segundo elemento del arreglo (con índice 1), y es en la primera posición del arreglo en donde coloca la cantidad de caracteres que asignó. Por ejemplo, si se quiere almacenar la siguiente cadena de caracteres: ‘Ana Cecilia Roncal Neyra’ El sistema colocará los caracteres en el arreglo de la siguiente manera: ↑
0
A 1
n 2
a 3
4
C 5
e 6
c 7
i 8
l 9
i 0 1
a 1
2
R 3
o 4
n 5
c 6
a 7
a 8
l 9
N 0 1 2
e 2
y 3
r 4
a 5
… 6
7
8
Nótese que en la posición 0 del arreglo se coloca el caracter ‘↑’ que corresponde al código 24 en la tabla ASCII, precisamente este valor coincide con la cantidad de caracteres colocados en el arreglo. Cualquier operación que se realice sobre la cadena afectará en forma automática la posición cero del arreglo de modo que en cada momento se tenga la cantidad de caracteres válidos almacenados. Esta forma práctica de manejar las cadenas de caracteres elimina el uso de la variable auxiliar, sin embargo tiene una limitante muy fuerte, es el hecho que bajo este esquema 211
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
sólo se pueden manejar cadenas que tengan más de 255 caracteres, esto simplemente porque en la posición cero del arreglo hay un elemento de un byte de tamaño, por lo tanto allí no puede caber un valor mayor a 255. Versiones más modernas de Pascal, por ejemplo en el Object Pascal esta estructura ha sido modificada para manejar cadenas de mayor tamaño, sin embargo no hay documentación al respecto sobre su estructura interna, esto se ha hecho con la finalidad que el programador se preocupe más por entender cómo se manejan las cadenas de caracteres sin preocuparse por su representación interna. En el caso del lenguaje C, se ha encontrado una forma muy práctica y sencilla de definir y manejar las cadenas de caracteres, en primer lugar una cadena de caracteres se define de igual manera cómo se define un arreglo unidimensional, no hay diferencias. En segundo lugar la manera de controlar la cantidad de caracteres válidos colocados en el arreglo se hace mediante un ‘caracter de terminación’, estos es , se ha elegido un caracter dentro de la tabla de caracteres que no se utilice dentro de cualquier texto y se coloca en la posición del arreglo inmediata al lado del último carácter. Este caracter es el caracter cuyo código ASCII es cero, conocido como ‘carácter nulo’. Por ejemplo, si se quiere almacenar la siguiente cadena de caracteres: ‘Ana Cecilia Roncal Neyra’
El sistema colocará los caracteres en el arreglo de la siguiente manera: A 0
n 1
a 2
3
C 4
e 5
c 6
i 7
l 8
i 9
a 0 1
1
R 2
o 3
n 4
c 5
a 6
a l 7 8
9
N 0 2
e 1
y 2
r 3
a ‘\0’ 4 5
6
7
8
9
‘\0’ representa al caracter cuyo código ASCII es cero
Esta forma de definir las cadenas de caracteres permite que prácticamente no haya límites en cuanto a la cantidad de caracteres que se pueda manejar en una variable. La forma cómo el lenguaje de programación define sus cadenas de caracteres se debe tomar en cuenta a la hora de plantear el algoritmo de solución del un problema que maneje una cadena caracter por caracter, esto debido a que mientras en Pascal esto se hace en función de la longitud de la cadena, en C se debe hacer hasta que se encuentre el caracter de terminación. El no tomar en cuentea este detalle, hará que el proceso se vuelva ineficiente.
Declaración de una cadena de caracteres Existen dos formas de definir una cadena de caracteres en Pascal, las mostramos a continuación: var cad1: string; cad2: string[20]; {el valor entre los [ ] indica el número máximo de caracteres que podrá almacenar la variable, en este caso 20}
En el caso de una definición como la de la segunda línea, se recomienda que se declare un tipo de dato con esas características y luego definir las variables en función de ese tipo. La razón es que esa forma de declaración se considera como una descripción de tipo, al igual que cuando se define un arreglo, y como ya hemos explicado anteriormente, 212
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
el Pascal no permite emplear esa forma en la declaración de parámetros en los procedimientos y funciones. Por lo tanto se debe realizar esta tarea de la siguiente manera: type St20 = String[20]; Cuando se quiere declara la variable se debe hacer de la siguiente manera: var cad: St20; En el caso de declarar un procedimiento o función: procedure proced(cad: String[20]); {Esto produce un ERROR de compilación} procedure proced(cad: St20); {Esto es CORRECTO}
Funciones y procedimientos que manejan cadenas: A continuación mostraremos una lista de funciones y procedimientos dados por el Pascal para el procesamiento de textos en un programa. FUNCIÓN LENGTH: esta función recibe como parámetro una cadena de caracteres y devuelve la cantidad de caracteres válidos que tiene la cadena. El siguiente ejemplo muestra cómo se maneja esta función. Ejemplo: var cad: String[20]; lon: Integer; begin writeln(' 1 2 3'); writeln(' 123456789012345678901234567890'); {Lo anterior dará una idea del tamaño de los textos}
cad := 'Hola amigos'; lon := length(cad); {Cuenta la cantidad de caracteres que hay en cad} writeln('Cadena 1: ', cad); writeln('Longitud: ', lon); writeln; cad := 'Ana Cecilia Roncal Neyra'; lon := length(cad); writeln('Cadena 2: ', cad); writeln('Longitud: ', lon); writeln; end. La ejecución de este programa tendrá una salida como la que se muestra a continuación:
213
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
FUNCIÓN CONCAT: esta función recibe como parámetro dos o más cadenas de caracteres, la función une o concatena las cadenas en el orden en que ingresan formando una única cadena y la devuelve. Se tiene que tener cuidado cuando la respuesta es asignada a una cadena acotada (variable definida con menos de 255 caracteres), si la concatenación es más grande que la variable que recibe la respuesta, la cadena se trunca, asignándose sólo los caracteres que puede contener la variable, esto no produce errores en la ejecución del programa. Otra cosa que s debe tener en cuenta es que la función no agrega ni quita caracteres a la concatenación. En el siguiente ejemplo se muestra cómo trabaja esta función. var cad1, cad2, cad3, cad4, cad5: String[50]; cad6: String[20]; begin cad1 := 'Juan'; cad2 := 'Perez'; cad3 := 'Cardenas'; cad4 := concat(cad1,cad2); writeln('Cadena 1: ', cad4); writeln; cad5 := concat('Resultado = ', cad1, ' / ', cad2, ' - ', cad3); writeln('Cadena 2: ', cad5); writeln; cad6 := concat('Resultado = ', cad1, ' / ', cad2, ' - ', cad3); writeln('Cadena 3: ', cad6); end. La salida a la ejecución del programa será como se muestra a continuación:
Esta función trabaja de manera similar a la forma como trabaja el operados + de concatenación. FUNCIÓN COPY: Esta función sirve para poder tomar una copia de una parte de una cadena de caracteres. La función recibe tres parámetros, el primero es la cadena de la que se quiere extraer la copia, el segundo argumento indica la posición de inicio de la copia, esta posición está dada por el índice en el arreglo dónde está el primer caracteres de la copia que se quiere extraer, y el tercer argumento indica la cantidad de caracteres a extraer a partir de la posición dada en el segundo argumento. El ejemplo siguiente muestra estas propiedades. program FuncionCopy; var cad1, cad2, cad3, cad4: String[30]; begin 214
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
writeln(' 1 2 3'); writeln(' 123456789012345678901234567890'); cad1 := 'Hola amigos, como estan'; cad2 := copy(cad1, 6, 5); {Copiar desde la posición 6, 5 caracteres} cad3 := copy(cad1, 8, 11); {Copiar desde la posición 8, 11 caracteres} cad4 := copy(cad1, 1, 6); {Copiar desde la posición 1, 6 caracteres} writeln('Cadena 1 : ', cad1); writeln; writeln('Cadena 2 ( 6, 5): ', cad2); writeln; writeln('Cadena 3 ( 8, 11): ', cad3); writeln; writeln('Cadena 4 ( 1, 6): ', cad4); end. La salida a la ejecución del programa será como se muestra a continuación:
En ésta y en la mayoría de funciones y procedimientos que manejan cadenas de caracteres en Pascal, no se hacen verificaciones sobre el rango en que se colocan los argumentos numéricos, esto quiere decir que si estos argumentos no son coherentes con la cadena dato, la función no enviará ninguna alerta ni cortará la ejecución del programa, siempre entregará un resultado que esté dentro de la lógica para los datos dados. Observe el ejemplo siguiente: program FuncionCopy2; var cad1, cad2, cad3, cad4: String[40]; begin writeln(' 1 2 3 4' ); writeln(' 1234567890123456789012345678901234567890 '); cad1 := 'Hola amigos, como estan'; cad2 := copy(cad1, 19, 50); {Copiar desde la posición 19, 50 caracteres} {La posición es correcta pero el número de caracteres sobrepasa el tamaño}
cad3 := copy(cad1, 35, 5); {Copiar desde la posición 35, 5 caracteres} {La posición es sobrepasa el número de caracteres que tiene la cadena}
cad4 := copy(cad1, -3, 6); {Copiar desde la posición -3, 6 caracteres} {Ilógica la posición dada}
writeln('Cadena 1 : ', cad1); writeln; writeln('Cadena 2 (19, 50): ', cad2); writeln; writeln('Cadena 3 (35, 5): ', cad3); writeln; 215
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
writeln('Cadena 4 (-3, 6): ', cad4); end. Veamos la salida a este programa:
FUNCIÓN POS: Esta función busca la posición en que una cadena se encuentra dentro de otra más grande, devuelve la posición del primer caracter en la cadena mayor. Si la cadena buscada se encuentra varias veces en la cadena mayor, la función siempre devolverá la primera ocurrencia. La función pos tiene dos argumentos, el primero es la cadena que se quiere localizar, la segunda es la cadena en donde se va a buscar la primera. La función devuelve un número entero con la posición encontrada, si no la encuentra devuelve cero. El programa siguiente muestra su funcionamiento program FuncionPos; var cad: String[50]; p1, p2, p3: Integer; begin writeln(' 1 2 3 4'); writeln(' 1234567890123456789012345678901234567890'); cad := 'Valentina busca a Naomi en el parque'; p1 := pos('Naomi', cad); {Busca la posición de 'Naomi en la cadena} p2 := pos('Juan' , cad); {Busca la posición de 'Juan' en la cadena} p3 := pos('NAOMI', cad); {La ocurrencia debe darse de manera exacta} writeln('Cadena: ', cad); writeln; writeln('Posicion de Naomi: ', p1:2); writeln; writeln('Posicion de Juan: ', p2:2); writeln; writeln('Posicion de NAOMI: ', p3:2); end. Veamos lo que muestra el programa
PROCEDIMIENTO INSERT: Este procedimiento modifica una cadena dada como argumento, la modificación consiste en insertar otra cadena en ella a partir de una posición dada. El procedimiento tiene tres parámetros, el primero es la cadena que se 216
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
quiere insertar, el segundo, parámetro por referencia, es la cadena que se quiere modificar, y el tercero es la posición donde se quiere hacer la inserción. En el programa siguiente se aprecian estas características. program procedimientoInsert; var cad: String[80]; begin writeln(' 1 2 3 4 5 6'); writeln(' 123456789012345678901234567890123456789012345678901234567890'); cad := 'Naomi estudia con Valentina'; writeln('Cadena Inicial: ',cad); writeln; insert('Ciencias de la Computacion ', cad, 15); writeln('Cadena Final : ',cad); end. La salida de este programa se muestra a continuación:
Al igual que en casos anteriores se debe tener en cuenta que si la cadena que se desea modificar es de menor tamaño que la longitud de la cadena que se obtendría como respuesta, el resultado se truncará. Por ejemplo: program procedimientoInsert; var cad: String[30]; --begin writeln(' 1 2 3 4 5 6'); writeln(' 123456789012345678901234567890123456789012345678901234567890'); cad := 'Naomi estudia con Valentina'; writeln('Cadena Inicial: ',cad); writeln; insert('Ciencias de la Computacion ', cad, 15); writeln('Cadena Final : ',cad); end. El resultado es el siguiente
Finalmente con este procedimiento tampoco se mostrarán mensajes o se truncará el programa si se dan datos incoherentes, veamos esto. program ProcedimientoInsert2; var cad, cad2: String[80]; begin 217
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo 1 2 3 4 5 6'); 123456789012345678901234567890123456789012345678901234567890');
writeln(' writeln(' cad := 'Naomi estudia con Valentina'; writeln('Cadena Inicial: ',cad); writeln; insert('Ciencias de la Computacion ', cad, 50); {Aquí la cadena se inserta al final}
writeln('Cadena Final : ',cad); writeln; writeln; cad2 := 'Naomi estudia con Valentina'; writeln('Cadena Inicial 2: ',cad2); writeln; insert('Ciencias de la Computacion ', cad2, -5); {Aquí la cadena se inserta al inicio}
writeln('Cadena Final 2 : ',cad2); end.
PROCEDIMIENTO DELETE: Este procedimiento también modifica una cadena ingresada como parámetro, la modificación consiste en eliminar un grupo de caracteres de la cadena. El procedimiento tiene 3 parámetros, el primero es la cadena que se quiere modificar, el segundo es un valor entero que indica la posición en el arreglo desde donde se eliminarán los caracteres, el último, también de valor entero, indica la cantidad de caracteres que se van a eliminar desde la posición indicada. El programa siguiente muestra cómo se maneja este procedimiento. program ProcedimientoDelete; var cad : String[50]; begin writeln(' 1 2 3'); writeln(' 123456789012345678901234567890'); cad := 'Hola a todos mis amigos'; writeln('Cadena Inicial: ',cad); writeln; delete(cad, 6, 12); writeln('Cadena Final : ',cad); end. La salida es la siguiente.
218
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Como en los casos anteriores datos incoherentes harán que la respuesta se adapte a la cadena ingresada como parámetro sin emitir mensajes de error o truncar el programa. program ProcedimientoDelete2; var cad, cad2, cad3: String[50]; begin writeln(' 1 2 3'); writeln(' 123456789012345678901234567890'); cad := 'Hola a todos mis amigos'; writeln('Cadena Inicial 1: ',cad); delete(cad, 10, 120); {Borra todos los caracteres desde la posición 10}
writeln('Cadena Final 1 : ',cad); writeln; writeln; cad2 := 'Hola a todos mis amigos'; writeln('Cadena Inicial 2: ',cad2); delete(cad2, 60, 7); {No borra los caracteres}
writeln('Cadena Final 2 : ',cad2); writeln; writeln; cad3 := 'Hola a todos mis amigos'; writeln('Cadena Inicial 3: ',cad3); delete(cad3, -6, 7); {No borra los caracteres}
writeln('Cadena Final 3 : ',cad3); end. Veamos la salida.
PROCEDIMIENTO VAL: Este procedimiento sirve para transformar una cadena de caracteres que represente un valor numérico en un número propiamente dicho. Expresado en otras palabras, un número que está representado como una cadena de caracteres podrá ser asignado a una variable numérica, entera o real. El procedimiento tiene tres argumentos, el primero es la cadena de caracteres que se quiere convertir, el segundo argumento es la variable numérica que recibirá el valor convertido, esta variable puede ser de tipo entero o real, la respuesta que de este procedimiento cambiará en función del tipo de variable que se emplee aquí. El tercer argumento es opcional, esto quiere decir que se puede o no utilizar en el procedimiento, este argumento debe ser de tipo entero y en él se almacenará un valor que nos indique si la cadena a transformar representa o no un valor numérico; si el valor asignado es cero quiere decir que la transformación ha sido exitosa, si el valor es diferente de cero 219
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
indica que la cadena no representa un dato numérico, el valor asignado indicará la posición del caracter en la cadena que no pudo convertir, en estos no se asignan valores al segundo argumento. En el programa siguiente veremos varios casos en el funcionamiento de este procedimiento. program ProcedimientoVal ProcedimientoVal;; var cadI cadI,, cadR cadR,, cad2 cad2,, cad3 cad3,, cad4 cad4,, cad5 cad5:: String String[[20 20]; ]; valorI,, codErr valorI codErr,, valor2 valor2:: Integer Integer;; valorR,, valor3 valorR valor3,, valor4 valor4,, valor5 valor5:: Real Real;; begin cadI := '437' '437';; cadR := cadR := '629.34' '629.34';; writeln(('1ra Parte:'); writeln Parte:'); writeln(('Valor entero asignado a variable entera'); writeln entera'); val((cadI val cadI,, valorI valorI,, codErr codErr); ); writeln(('CadenaI: ', cadI writeln cadI::15 15,, ' ValorI: ', valorI valorI::8, ' Error: ', codErr codErr); ); writeln;; writeln writeln(('Valor real asignado a variable real'); writeln real'); val((cadR val cadR,, valorR valorR,, codErr codErr); ); writeln(('CadenaR:', cadR writeln cadR::15 15,, ' ValorR:', valorR valorR::8:3, ' Error: ', codErr codErr); ); writeln;; writeln writeln writeln;; writeln(('2da Parte:'); writeln Parte:'); writeln(('Valor entero asignado a variable writeln vari able real'); real'); val((cadI val cadI,, valorR valorR,, codErr codErr); ); writeln(('CadenaI:', cadI writeln cadI::15 15,, ' ValorR:', valorR valorR::8:3, ' Error: ', codErr codErr); ); writeln;; writeln writeln(('Valor real asignado a variable entera'); writeln entera'); val((cadR val cadR,, valorI valorI,, codErr codErr); ); writeln(('CadenaR: ', cadR writeln cadR::15 15,, ' ValorI: ', valorI valorI::8, ' Error: ', codErr codErr); ); writeln(('***El codigo de error coincide con la posicion del punto***'); writeln punto***'); writeln;; writeln writeln writeln;; writeln(('3ra Parte:'); writeln Parte:'); writeln(('La cadena no representa un valor numerico'); writeln numerico'); cad2 := cad2 := '45Ax23' '45Ax23';; val((cad2 val cad2,, valor2 valor2,, codErr codErr); ); writeln(('Cadena : ', cad2 writeln cad2::15 15,, ' Valor : ', valor2 valor2::8, ' Error: ', codErr codErr); ); writeln;; writeln cad3 := cad3 := '45.23.56' '45.23.56';; val((cad3 val cad3,, valor3 valor3,, codErr codErr); ); writeln(('Cadena : ', cad3 writeln cad3::15 15,, ' Valor :', valor3 valor3::8:3, ' Error: ', codErr codErr); ); writeln;; writeln writeln(('La cadena contiene espacios al inicio'); writeln inicio' ); cad4 := cad4 := ' 453.56'; 453.56'; val((cad4 val cad4,, valor4 valor4,, codErr codErr); ); 220
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
writeln('Cadena : ', cad4 writeln( cad4::15 15,, ' Valor :', valor4 valor4::8:3, ' Error: ', codErr codErr); ); writeln(('***No se debe producir un error***'); writeln error***'); writeln;; writeln writeln(('La cadena contiene espacios al final'); writeln final' ); cad5 := cad5 := '453.56 '; '; val((cad5 val cad5,, valor5 valor5,, codErr codErr); ); writeln(('Cadena : ', cad5 writeln cad5::15 15,, ' Valor :', valor5 valor5::8:3, ' Error: ', codErr codErr); ); writeln(('***Se debe producir un error***'); writeln error***'); writeln;; writeln writeln writeln;; end.. end Veamos lo que se obtiene
PROCEDIMIENTO STR: Este procedimiento busca realizar una tarea inversa al del procedimiento val, esto es convertir un número en una cadena de caracteres. Este procedimiento recibe dos argumentos, el primero es una variable que puede ser entero o real que contiene el número que se desea transformar, el segundo es la cadena que recibe el valor convertido. La particularidad de este procedimiento es que la cadena final tendrá el mismo formato que tendría el número si se lo enviáramos como parámetro al procedimiento write, esto es, si se trata de un valor real la cadena resultante saldría en notación científica, por lo que el valor entregado como dato podrá tener especificadores de formato. El programa siguiente sig uiente muestra estos detalles. program ProcedimientoStr ProcedimientoStr;; var cadI cadI,, cadR cadR:: String String[[30 30]; ]; valorI:: Integer valorI Integer;; valorR:: Real valorR Real;; begin valorI := valorI := 374 374;; valorR := valorR := 381.79 381.79;; writeln(('Sin formato:'); writeln formato:'); str((valorI str valorI,, cadI cadI); ); str((valorR str valorR,, cadR cadR); ); 221
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
writeln('ValorI: ', writeln( ', valorI valorI::10 10,, ' writeln(('ValorR: ', writeln ', valorR valorR::10 10::3, writeln;; writeln writeln writeln;; writeln((' writeln writeln(('Con formato: writeln str((valorI str valorI::10 10,, cadI cadI); ); str((valorR str valorR::10 10::4, cadR cadR); ); writeln(('ValorI: ', writeln ', valorI valorI::10 10,, ' writeln(('ValorR: ', writeln ', valorR valorR::10 10::3, writeln;; writeln writeln writeln;; end.. end Veamos la salida.
CadenaI: ', cadI cadI); ); ' CadenaR: ', cadR cadR); ); 1 2'); 2' ); 12345678901234567890'); 12345678901234567890' ); CadenaI: ', cadI cadI); ); ' CadenaR: ', cadR cadR); );
PROCEDIMIENTO SETLENGHT: Este procedimiento no está implementado en el Pascal estándar, recién aparece en versiones de Object Pascal debido a la manera en que son implementadas las cadenas de caracteres en estas versiones. Con este procedimiento se establece un tamaño inicial para una cadena de caracteres no acotada. Si se quiere asignar caracteres a una cadena como si fuera un arreglo, pero la cadena no ha sido inicializada previamente con un texto, el sistema obligará a emplear este procedimiento antes de utilizar la cadena como arreglo, de no hacerlo se emitirá un error de compilación. Se debe tener en cuenta que el error se produce al asignar un caracter a una posición determinada, no al emplear un elemento del arreglo. El procedimiento tiene dos argumentos, el primero es la cadena a la que se le quiere definir su tamaño, el segundo es un valor entero que contiene el tamaño deseado. Veamos su uso. program ProcedimientoSetLength ProcedimientoSetLength;; var cad cad,, cad2 cad2:: String String;; c: Integer Integer;; begin cad := cad := 'Hola amigos'; amigos'; setLength((cad2 setLength cad2,, length length((cad cad)); )); // El proceso siguiente no debe compilar si eliminamos // la línea que contiene el procedimiento SetLength
for c := 1 to length length((cad cad)) do cad2[[c] := cad cad2 cad[[c]; writeln(('Cadena inicial: ', cad writeln cad); ); writeln(('Cadena asignada: ', writeln ', cad2 cad2); ); end.. end
222
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Aplicaciones que emplean cadenas de caracteres: Veamos ahora algunas aplicaciones que permitan procesar textos. Invertir un texto Aquí veremos dos formas de invertir el contenido de una cadena de caracteres, la primera emplearemos la función concat concat,, la segunda se manejarán las cadenas como arreglos. program InvierteCadena InvierteCadena;; var cad cad,, cadInv1 cadInv1,, cadInv2 cadInv2:: String String;; c, c2 c2:: Integer Integer;; begin cad := 'Hola amigos, como estan'; estan'; // Primera versión: Usando CONCAT
cadInv1 := '' '';;
// Aquí se emplearán los elementos del arreglo // por lo que no se producen errores
for c := length length((cad cad)) downto 1 do cadInv1 := cadInv1 := concat concat((cadInv1 cadInv1,, cad cad[[c]);
// Esto es equivalente a hacer: cadInv1 := cadInv1 + cad[i];
writeln(('Cadena original: ', writeln ',cad cad,, ' Cadena 1: ',cadInv1 cadInv1); ); // Segunda versión: Usando los elementos del arreglo
cadInv2 := '' '';; setLength((cadInv2 setLength cadInv2,,length length((cad cad)); ));
// De no emplearse la línea anterior producirá un error // en la asignación de los elementos de la cadena cad ena
c2 := length c2 := length((cad cad); ); for c := 1 to length length((cad cad)) do begin cadInv2[[c2 cadInv2 c2]] := cad cad[[c]; dec((c2 dec c2); ); end;; end writeln(('Cadena original: ', writeln ',cad cad,, ' Cadena 2: ',cadInv2 cadInv2); ); end.. end
Buscar y reemplazar una cadena por otra La idea de este ejemplo es la de buscar un texto dentro de una cadena de caracteres y reemplazarlo por otro. No habrá restricciones entre la longitud del texto que sale y del que entra, esto quiere decir que el texto que sale podrá ser de mayor, menor o igual ig ual que el que sale. Aquí también mostraremos la solución de dos maneras, una empleando la 223
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
función copy copy y y la otra empleando los procedimientos delete e insert insert.. En ambos caso se ubicará el texto con la función pos pos.. En el gráfico siguiente mostramos las tareas que se desean realizar en el programa. Texto buscado C a d e n a
d e
c a r a c t e r e s
1er paso: buscamos el texto con la función pos pos:: Texto buscado Parte izquierda
Texto buscado P a r t e d e r e c h a Posición P
Versión 1: Usando Copy Parte izquierda
+
Texto de reemplazo
+ Parte derecha
Versión 2: Usando Insert Insert y Delete Parte izquierda
Texto buscado P a r t e d e r e c h a Posición P
Delete
Texto buscado
Parte izquierda
Parte derecha Posición P
Insert
Texto de reemplazo
Parte izquierda
Texto de reemplazo P a r t e d e r e c h a Posición P
Veamos el programa: program BuscarYReemplazarTextos BuscarYReemplazarTextos;; var cad1 cad1,, cad2 cad2,, cadOut cadOut,, cadIn cadIn:: String String;; p: Integer Integer;; begin writeln(('Version 1 (copy):'); writeln (copy):'); cad1 := cad1 := 'Curso de programacion de EE GG CC de la PUCP' PUCP';; cadOut := cadOut := 'EE GG CC'; CC'; cadIn := cadIn := 'la Faciltad de Ciencias e Ingenieria'; Ingenieria'; writeln(('Texto incial:'); writeln incial:'); writeln((cad1 writeln cad1::60 60); ); p:= pos pos((cadOut cadOut,, cad1 cad1); ); if p>0 then cad1 := cad1 := copy copy((cad1 cad1,, 1, p-1) // Parte izquierda + cadIn // Texto de reemplazo 224
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
+ copy(cad1, p+length(cadOut), 255); // Parte derecha // No se requiere calcular la cantidad de caracteres de la parte // derecha, sólo se debe colocar un valor suficientemente grande
writeln('Texto reemplazado:'); writeln(cad1:75); writeln; writeln('Version 2 (insert y delete):'); cad2 := 'Curso de programacion de EE GG CC de la PUCP'; cadOut := 'EE GG CC'; cadIn := 'la Faciltad de Ciencias e Ingenieria'; writeln('Texto incial:'); writeln(cad2:60); p := pos(cadOut, cad2); if p > 0 then begin delete(cad2, p, length(cadOut)); // Borramos el texto buscado insert(cadIn, cad2, p); // Insertamos el texto de reemplazo end; writeln('Texto reemplazado:'); writeln(cad2:75); end.
Verificación en el ingreso de datos En ejemplos anteriores hemos presentado situaciones en las que se requiere de la verificación de los datos a la hora de ingresarlos al programa, por ejemplo si se desea ingresar la nota de un alumno y ésta requiere que esté entre 0 y 20, podríamos escribir el siguiente código: var nota: Integer; begin … writeln(‘Ingrese un valor entre 0 y 20:’); repeat readln(nota); if not (nota in [0..20]) then begin writeln(‘ERROR: Debe ingresar un valor entre 0 y 20’); writeln(‘Ingrese un nuevo valor’ ); end; until nota in [0..20]; … Este programa, como ya hemos visto, funciona adecuadamente para cualquier valor numérico entero que se ingrese, si ingresamos un valor como 18, el programa continúa sin novedad, si ingresamos un valor como 25, aparece en la ventana de salida del programa 225
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
un mensaje indicándonos que no se ha ingresado un valor correcto y nos permite ingresar un nuevo valor, si se sigue ingresando valores incorrectos, el mensaje seguirá apareciendo, esto hasta que se ingrese un valor en el rango esperado, luego de esto el programa continua. A pesar de esto, ¿qué cree usted que pasaría si ingresamos un valor de tipo real como por ejemplo 12.5?, ¿qué pasaría si ingresamos un dato como 2x4? La respuesta a esto es simple, el programa se interrumpiría, aparecería en la ventana de salida un mensaje de error diferente al que hemos puesto en el código y no podría continuar la ejecución del programa. En la ventana de salida el mensaje sería similar al siguiente:
Esta situación es grave ya que el programa termina en ese punto, perdiéndose todo lo avanzado o realizado por el programa hasta ese momento. El empleo de cadenas de caracteres en este caso puede ser muy útil para manejar estas situaciones. La idea es que el dato sea recibido por una variable de tipo cadena de caracteres en vez de una variable numérica. Al hacerse así, por la naturaleza de la variable, cualquier cosa que se ingrese podrá ser almacenada en la variable, de esta forma el programa no se detendrá por un error de entrada. Luego de recibir la información, el código del programa será capaz de decidir si el dato es adecuado y continuar con el mismo, o enviar una alerta de error y solicitar un nuevo dato, pero sin interrumpir el programa. El código siguiente muestra como se puede hacer esto: program VerificarDatoConStr; type Str15 = String[15]; var notaStr: Str15; nota, error: Integer; begin writeln('Ingrese un valor entre 0 y 20:'); repeat readln(notaStr); //Intentamos convertir el texto a un numero entero
val(notaStr, nota, error); if error <> 0 then begin writeln('ERROR: Debe ingresar un valor numerico entero'); writeln('Ingrese un nuevo valor'); end else if not (nota in [0..20]) then begin writeln('ERROR: Debe ingresar un valor entre 0 y 20'); writeln('Ingrese un nuevo valor'); end; until (error = 0) and (nota in [0..20]); 226
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
writeln('Dato correcto = ', nota); end. El código siguiente muestra como se puede hacer esto:
Extrae palabras de una cadena de caracteres Otra tarea que se presenta frecuentemente en los problemas en los que interviene el uso de cadenas de caracteres es la de la descomposición de la cadena en palabras. El método que emplearemos se basará en la implementación de una función a la que se le ingresará la cadena de caracteres como parámetro y nos devolverá una palabra, siguiente a la última que salió. El programa siguiente muestra el código del programa principal que ilustra lo que queremos hacer. var cad, palabra: String; begin // Partimos de una cadena que tenga varios espacios en blanco entre palabras
cad := ' Esta es una prueba de como se extraen writeln('Antes:'); writeln(' Cadena: ', cad); writeln; writeln('Palabras de la cadena: '); repeat palabra := sacaPalabra(cad); if palabra <> '' then writeln(palabra); until palabra = '';
palabras de una cadena ' ;
// Mostramos como queda la cadena original después del proceso
writeln; writeln('Despues:'); writeln(' Cadena: ', cad); end. Al igual que con alguno de los ejemplos anteriores, plantearemos dos maneras de resolverlo. En la primera, veremos cómo las limitaciones presentes en el leguaje de programación nos llevan, como veremos, a una solución poco elegante, en la segunda empelaremos el concepto de ‘variable estática ’, definida anteriormente. En ambos casos simplificaremos el problema restringiéndolo al hecho que las palabras están separadas solamente por espacios en blanco; en base a este algoritmo se podrá extender la complejidad del problema a otros separadores como tabuladores, signos de puntuación, etc. 227
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Versión 1: Como indicamos, en esta versión tendremos que adaptarnos a las limitaciones del lenguaje. En este sentido, tenemos que ver que la manera de poder extraer una palabra de la cadena estará determinando por la búsqueda de espacios en blanco, delimitadores de las palabras. Aquí es donde se enfoca la limitación; resulta que la función de búsqueda que nos proporciona el lenguaje es la función pos, y como hemos visto, esta función nos entrega la ubicación de la primera ocurrencia. Por lo tanto el algoritmo deberá eliminar los espacios en blanco que tenga la cadena antes de la primera palabra y, usando la función pos, buscar el siguiente espacio en blanco, lo que nos dará la extensión de la primera palabra para poder extraerla con la función copy, que en realidad nos entrega una copia de la palabra, luego para poder sacar la siguiente palabra, deberemos borrar la palabra ‘extraída’ de la cadena para repetir el proceso.
Como se puede apreciar para realizar la tarea de extraer las palabras de una cadena debemos destruir poco a poca la cadena original. Aquí es donde radica el hecho que consideremos poco elegante el método, ya que finalmente la cadena original quedará destruida. Esto se tratará de evitar en la segunda versión. A continuación mostramos la función que implementa la primera versión. function sacaPalabra(var cad: String): String; // El parámetro debe ser por referencia porque se modificará la cadena // argumento, luego de entregar el valor.
var p: integer; begin
// Borramos los espacios que se encuentren antes de la primera palabra. // El proceso iterativo primero verifica si la cadena tiene caracteres y // luego si el primer caracteres es espacio en blanco, si lo es lo borra.
while (cad <> '') and (cad[1] = ' ') do delete(cad, 1, 1);
// En este punto la cadena no tiene espacios en blanco iniciales, por lo // que procedemos a buscar el espacio en blanco que delimitará la palabra.
p := pos(' ', cad);
// Si la función pos devuelve cero quiere decir que la cadena sólo tiene // una palabra o que está vacía.
if p = 0 then begin
// Sacamos la última palabra y limpiamos la cadena para que el // proceso termine
sacaPalabra := cad; cad := '';
end else begin
// Sacamos la primera palabra de la cadena y la // borramos
sacaPalabra := copy(cad, 1, p-1); delete(cad, 1, p);
end end; La salida de este programa será como se muestra a continuación. 228
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Observe que la cadena original fue destruida en proceso. Versión 2: En esta versión trataremos de implementar una solución que no termine con la destrucción de la cadena original. Para hacer esto entonces no podemos basarnos en el empleo de la función pos, por lo menos no de manera principal. Lo que tenemos que hacer es guardar de alguna manera la posición del espacio en blanco que delimito el fin de la palabra anterior, si esto se consigue se podrá buscar entonces el inicio de la siguiente palabra y por lo tanto también el final de la palabra. El problema aquí es cómo guardamos esa posición. Un programador novato plantearía la solución definiendo una variable en el programa principal en la que se guarde la posición final de la última palabra, luego tendría que pasar esta variable como parámetro por referencia a la función que extrae la siguiente palabra para que ella, a partir de allí la saque. Esta solución no es buena, si lo vemos desde el punto de vista de la programación modular. La razón a esto es que debemos entender que la tarea de la función sacaPalabra es la de entregarnos la siguiente palabra de la cadena, si esto lo vemos desde la perspectiva del que hace el programa principal, podemos decir que a él sólo le interesa que le entregue la palabra para poderla procesar en la tarea que está solucionando, ¿cómo hace la función para entregarle la siguiente palabra?, pues no es su problema, esto es el principio del encapsulamiento. Si el programa principal va a definir una variable para controlar la posición de las palabras, pues simplemente estamos condicionando la forma cómo va a plantearse el algoritmo de solución del sacaPalabra, y por otro lado estamos aliviando el trabajo que debe hacer el equipos que se encargue de implementar la función. Esto no debe pasar, por lo tanto el planteamiento de solución de la función sacaPalabra, debe enfocarse en la idea de entregar la siguiente palabra de una cadena, único parámetro de la función. Por esta razón es que en esta versión emplearemos ‘variables estáticas’, las cuales como hemos visto son variable locales a la función pero no se destruyen luego de ejecutar la función y por lo tanto el valor final que tuvo luego de la ejecución anterior se mantiene en la actual ejecución. Entonces en la implementación de esta versión de la función definiéremos una variable estática que permita almacenar la posición del espacio que marca el final de la última palabra extraída. 229
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Luego debemos analizar una situación que bajo estas condiciones no se presenta en la primera versión. En la versión 1, la función devuelve, literalmente, siempre la primera palabra de la cadena, por lo tanto en este caso si el programa tuviera la tarea de extraer unas cuantas palabras de la cadena, no todas, y luego se quisiera extraer las palabras de otra cadena, lo único que habría que hacer es cambiar la cadena que ingresa como parámetro. Sin embargo el cambiar la cadena que ingresa como parámetro en esta nueva versión ocasionaría un error, esto debido a que la palabra que se extraería en este caso se hará a partir del valor que tiene la variable estática, valor que no permitirá sacarlas desde la primera palabra. Por esto se debe buscar un mecanismo que permita detectar, dentro de la misma función, que se está ingresando o no una cadena de caracteres diferente a la que se usó en la anterior llamada. La solución que planteamos aquí se basará en el uso de otra variable estática, definida como una cadena de caracteres, esta variable en la primera llamada a la función se inicializará con una cadena vacía. Lo que se hará a partir de aquí será comparar esta variable estática con la cadena entrada como parámetro, si son diferentes, como es el caso de la primera llamada a la función, entonces la variable estática definida para determinar los límites de las palabras se inicializará para que empiece a sacar las palabras desde el inicio, luego se le asignará a la cadena de caracteres, definida como variable estática, el valor de la cadena pasada como parámetro para que mantenga ese valor en la siguiente llamada. A partir de la segunda llamada a la función la cadena de caracteres estática se compara nuevamente con la cadena parámetro, si son iguales, el proceso extraerá la siguiente palabra, partiendo del valor que tiene la variable estática que delimita las palabras, si son diferentes se procede cono si se tratara de la primara llamada a la función. Veamos el código: function sacaPalabra(cadena: String): String; // Variables estáticas:
const control: String = ''; // Controla si se cambió la cadena parámetro delimitador: Integer = 0; // Servirá para delimitar las palabras var cadAux: String; posIni, cantCar: Integer; begin // Verificamos si el contenido de la variable de control estática // coincide con el contenido del argumento
if cadena <> control then begin
// Si son diferentes, se establece el delimitador de palabras // en el primer caracter de la cadena y ...
delimitador := 1;
// ... se guarda la nueva cadena para ser comparada en el // siguiente llamado a la función
control := cadena; end;
// si las cadenas son iguales se mantiene el valor del delimitador // que se estableció en el llamado anterior
230
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
// Movemos la delimitador de modo de saltar los espacios en //blanco iniciales
while (delimitador <= length(cadena)) and (cadena[delimitador] = ' ') do inc(delimitador); // Marcamos el inicio de la palabra
posIni := delimitador;
// Buscamos el fin de la palabra
cantCar:= 0; while (delimitador ' ') do begin inc(delimitador); inc(cantCar); end; sacaPalabra := copy(cadena, posIni, cantCar); // Si la extracción de la palabra da una cadena vacía, indicará // que se sacaron ya todas las palabras de la cadena.
end; La salida a este programa será la siguiente:
Observe que la cadena original queda intacta en proceso. Manipulación de números mixtos Se desea elaborar un programa que permita realizar operaciones con números mixtos. La idea es poder extraer los valores desde cadenas de caracteres, por lo que la fuente de datos será un archivo de textos con un formato peculiar. Se tiene un archivo de textos como se muestra a continuación: Expresión que contiene varios quebrados (D): 3 4/5 + 371 - 4 101/710 + 12 2/9012 + 3/5… Sumatoria de valores compuestos: 11/7 - 2 2/5 - 5 + 11 12/23 + 3 5/6… Lista de datos (Q): 1 1/5 + 1 1/5 + 1 1/5 + 1 1/5 + 1 1/5… …
El archivo contiene en cada línea una lista de números mixtos colocados a modo de una expresión. Los operadores que se emplean en estas expresiones son únicamente la suma (+) y la resta (-). Los números mixtos estarán dados por un número entero y/o una fracción, esto es que puede aparecer sólo un entero (p. e.: 5), sólo un quebrado o 231
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
fracción (p. e.: 3/5) o un número mixto (p. e.: 3 1/4). No hay límite en la cantidad de cifras que puede tener un número. Cada línea empieza con un texto que termina con el carácter “:”. Antes de los dos puntos puede aparecer entre paréntesis las letras “D”, “Q” o “M”. Se requiere un programa que permita obtener el resultado de las expresiones en cada línea. El resultado deberá presentarse con un formato que dependa de la letra que aparece al final del texto, esto es si aparece la D el resultado se presentará en un formato decimal (p. e.: 2.875), si aparece la Q se expresará como quebrado simple (p. e.: 23/8) y si aparece la letra M se deberá mostrar como un número mixto (p. e.: 2 7/8), en los dos últimos casos los quebrados deben aparecer simplificados esto es, no deben aparecer valores como 2/4 ó 6/9, en vez de eso debe aparecer 1/2 ó 2/3. En el caso que no aparezca la letra, el formato que se aplicará será el mismo que apareció en la línea anterior. En la primera línea siempre aparecerá una letra. Solución: La solución la plantearemos de la siguiente manera: leeremos una línea completa del archivo y la almacenaremos en una cadena de caracteres. A partir de allí separaremos el texto inicial, el carácter con el formato de impresión y del resto de la cadena determinaremos el resultado. El valor que contiene la respuesta estará dado en dos variables, una contendrá el numerador y la otra el denominador de la fracción resultante, esto porque a partir de allí se pude determinar el valor decimal o el valor mixto. Si el valor resultante fuera sólo un número real, a partir de él no se podría determinar la fracción que lo genera, por la pérdida de precisión que se presenta en la representación de valores reales. Con esa información imprimiremos el resultado de la línea. El proceso se repite hasta completar todo el archivo. El código siguiente muestra esta solución: program ManipulacionDeNumerosMixtos; var linea, texto: String; formato: Char; numerador, denominador: Integer; arch: Text; begin assign(arch, 'datos.txt'); reset(arch); while not eof(arch) do begin readln(arch, linea); obtenerResultado(linea, texto, formato, numerador, denominador); imprime(texto, formato, numerador, denominador); end; close(arch); end. El procedimiento que se encargue de obtener los resultados deberá realizar lo siguiente: primero separará el texto inicial de la secuencia de valores que forman una expresión numérica de valores mixtos, esto se hará en dos cadenas de caracteres adicionales, para eso sólo habrá que buscar en la cadena el carácter ‘:’ y a partir de allí dividir la cadena 232
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
en dos. Luego del texto inicial se extraerá el caracter de formato, si no lo tiene se determinará el valor que tenía la línea anterior. El siguiente paso será el de descomponer la cadena que contiene la expresión numérica en cada uno de sus términos. Esto se hará considerando cada término como una palabra, en la que el signo de operación (+ ó -) será el separador entre cada una. Para hacer el proceso más sencillo colocaremos inicialmente cada término en los elementos de un arreglo de cadenas de caracteres. Por otro lado los signos de operación lo guardaremos en una cadena de caracteres a modo de arreglo de caracteres. Luego de la separación de los datos, se irán tomando uno a uno los términos de las expresiones, se transformará las cadenas en números y se acumulará según los signos de operación. La acumulación se hará a modo de quebrado, esto es manteniendo un valor para el numerador y otro para el denominador, luego de obtener la respuesta de la expresión se procederá a simplificar el quebrado. El código que plasma estas operaciones, se muestra a continuación: { Debemos definir primero el tipo de dato (arreglo de cadenas de caracteres) que manejará las palabras.}
const MAXQUEB = 100; type Str30 = String[30]; TMixto = array [1..MAXQUEB] of Str30; … procedure obtenerResultado( linea:String; var texto: String; var formato: Char; var numerador, denominador: Integer); var operandos: TMixto; // Guardará las palabras operaciones: String; // Guardará los signos de operación begin separaDatos(linea, texto, formato, operandos, operaciones);
{ Observe que el procedimiento separaDatos no se devuelve el número de operandos, este valor se manejará a través de la longitud de la cadena operaciones.}
resultado(operandos, operaciones, numerador, denominador); end; El procedimiento separaDatos se muestra a continuación: procedure separaDatos( linea: String; var texto: String; var formato: Char; var operandos: TMixto; var operaciones: String); var p, numOper: Integer; oper: String; begin
{ Primero eliminamos los espacios sobrantes, de modo que cada ‘palabra’ de la cadena se separe sólo por un espacio en blanco. Se considera en este ejemplo como ‘palabra’ a las palabras propiamente dichas, a los valores enteros, a los quebrados y a los signos de operación.}
233
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
borarEspaciosSobrantes(linea); // Inicializamos los datos que manejarán los operandos y operadores.
operaciones := ''; numOper := 0;
// Buscamos la separación entre el texto y las operaciones, y las separamos
p := pos(':', linea); texto := copy(linea, 1, p-1);
// Extraemos el formato que determina cómo saldrá el resultado.
obtenFormato(texto, formato); // Borramos el texto inicial.
delete(linea, 1, p);
// Extraemos los operandos y operaciones del resto de la cadena
p := 1; repeat
// Buscamos el siguiente operador en la cadena
while (p <= length(linea)) and not (linea[p] in ['+', '-']) do inc(p); if linea[p] in ['+', '-'] then begin // Encontramos un operador y lo guardamos
operaciones := operaciones + linea[p]; // Extraemos el operando
oper := copy(linea,1,p-2); { Ahora procederemos a estandarizar el operando, esto para que luego cuando tengamos que convertirlo a un número y lo operemos, esta tarea sea más sencilla}
estandariza(oper);
// Colocamos el operando en el arreglo
end else
inc(numOper); operandos[numOper] := oper;
// No encontramos un operador, luego sólo está el operando final
operandos[numOper+1] := linea;
// Borramos la parte de la cadena analizada
delete(linea, 1, p+1); p := 1; until linea = ''; end; El procedimiento borarEspaciosSobrantes se muestra a continuación: procedure borarEspaciosSobrantes(var linea: String); var p: Integer; begin // Buscamos una secuencia de dos espacios en blanco y eliminamos uno.
234
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
// De ese modo dejamos sólo un espacio entre palabras.
repeat p := pos(' ', linea); // busca 2 espacios en blanco … if p <> 0 then delete(linea, p, 1); // … y borramos uno until p = 0; // Verificamos si hay un blanco al inicio o al final y los eliminamos
if linea[1] = ' ' then delete(linea, 1, 1); if linea[length(linea)] = ' ' then delete(linea, length(linea), 1); end; El procedimiento obtenFormato se muestra a continuación: procedure obtenFormato(var texto: String; var formato: Char); var p: Integer; begin // Buscamos el paréntesis ‘(‘
p := pos('(', texto); if p <> 0 then begin
// Si lo encontramos, lo extraemos
formato := texto[p+1];
// Borramos los caracteres desde el paréntesis
delete(texto, p, 255); end;
{ Si no lo encontramos, la variable formato no es modificada, quedándose con el valor anterior.}
end; El procedimiento que estandariza el operando guardará el número mixto en un formato con la forma: “E N/D”; donde E será la parte entera del número (si el número no tiene parte entera se colocará un carácter cero), N es el numerador de la fracción del número (si no tiene parte fraccionaria se colocará un carácter cero) y D es el denominador de la parte fraccionaria del número (si no tiene parte fraccionaria colocará un carácter 1). De este modo un operandos como “35”, “7/15” ó “2 11/23” se estandarizará como “35 0/1”, “0 7/15” y “2 11/23” respectivamente. El procedimiento estandariza se muestra a continuación: procedure estandariza(var oper: String); var posBl, posSl: Integer; begin { Buscamos la posición del espacio en blanco y del slash ‘/’, si los ubicamos guardamos su posición en la cadena.}
posBl := pos(' ',oper); posSl := pos('/',oper);
{ Si encontramos el espacio en blanco entonces tendremos un número mixto como “2 11/23”y no habrá que procesarlo, si no lo encuentra será porque se trata de un entero como “35” o un quebrado como “7/15”.}
if posBl = 0 then begin
{ si encontramos el slash quiere decir que estamos ante un quebrado, si no será sólo un entero.}
235
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
if posSl = 0 then oper := oper + ' 0/1' else oper := '0 ' + oper; end; end; Ahora nos concentraremos en el procedimiento resultado, como se indicó anteriormente, aquí se irán tomando del arreglo uno a uno los términos de las expresiones, se transformará las cadenas en números y se acumulará según los signos de operación, manteniendo un valor para el numerador y otro para el denominador, luego de obtener el resultado se procederá a simplificar el quebrado. procedure resultado( var operandos: TMixto; operaciones: String; var numerador, denominador: Integer); var num, den, i: Integer; begin { Convertimos la cadena con el primer operando en un número quebrado, sobre estas variables se acumularán los otros operandos.}
aNumero(operandos[1], numerador, denominador);
{ Agregamos un espacio al inicio de la cadena de operadores para así hacer más fácil el manejo de los índices de los arreglos.}
operaciones := ' ' + operaciones;
{ Acumulamos el resto de los operandos según el operador.}
for i := 2 to length(operaciones) do begin aNumero(operandos[i], num, den); if operaciones[i] = '+' then numerador := numerador*den + num*denominador else numerador := numerador*den - num*denominador; denominador := denominador * den; end; { Reducimos o simplificamos el quebrado resultante.}
reduce(numerador, denominador); end; El procedimiento aNumero se muestra a continuación: procedure aNumero (operando:String; var numerador, denominador: Integer); var pBl, pSl, entero, codErr: Integer; ent, num, den: String; begin pBl:= pos(' ',operando); pSl:= pos('/',operando); ent:= copy(operando, 1, pBl-1); num:= copy(operando, pBl+1, pSl-pBl-1); 236
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
den:= copy(operando, pSl+1,255); val(num, numerador, codErr); val(den, denominador, codErr); val(ent, entero, codErr); numerador := entero*denominador + numerador; end; El procedimiento reduce se muestra a continuación: procedure reduce(var numerador, denominador: Integer); var factor: Integer; begin factor := 2; while (factor <= numerador div 2) or (factor <= denominador div 2) do begin if (numerador mod factor = 0) and (denominador mod factor = 0) then begin numerador := numerador div factor; denominador := denominador div factor; end else inc(factor); end; end; Finalmente procedemos a imprimir el resultado mediante el procedimiento imprime: procedure imprime ( texto: String; formato: Char; numerador, denominador: Integer); var i: Integer; begin write('Formato (',formato,'): '); write(texto); for i := 1 to 40-length(texto) do write(' '); if formato = 'D' then writeln(numerador/denominador:10:5) else if formato = 'Q' then writeln(numerador:4,'/',denominador) else writeln ( numerador div denominador:4,' ', numerador mod denominador,'/',denominador); end; Si ejecutamos el programa con un archivo como el que se muestra a continuación: Expresion que contiene varios quebrados (D): 3 4/5 + 371 Sumatoria de valores compuestos: 11/7 - 2 2/5 - 5 + Lista de datos (Q): 1 1/5 + 1 1/5 + 1 1/5 + 1 Expresion que contiene varios quebrados (M): 3 4/5 + 371 Sumatoria de valores compuestos: 11/7 - 2 2/5 - 5 + Lista de datos (D): 1 1/5 + 1 1/5 + 1 1/5 + 1
- 4 101/710 + 12 2/9012 + 3/5 11 12/23 + 3 5/6 1/5 + 1 1/5 - 4 101/710 + 12 2/9012 + 3/5 11 12/23 + 3 5/6 1/5 + 1 1/5
237
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Obtendremos el siguiente resultado:
238
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 10: Registros Definición Cuando se declara una variable en un programa, se está definiendo un espacio de memoria en la que se podrá almacenar un solo valor. Por ejemplo cuando colocamos en un programa la sentencia var a: Integer; estamos definiendo la variable entera a, la cual sólo podrá almacenar un valor en un instante dado. A este tipo de variable se le denomina escalar. Por otro lado cuando definimos un arreglo estamos definiendo un conjunto de variables, todas identificadas por un mismo nombre, diferenciándose cada una por uno o más índices, así por ejemplo cuando en un programa escribimos la sentencia var a: array [1..5] of Integer; estamos definiendo las variables enteras a[1], a[2], a[3], a[4] y a[5]. Hemos visto en los capítulos anteriores la cantidad de aplicaciones que podemos desarrollar con este tipo de dato, sin embargo, a pesar de ello los arreglos tienen un inconveniente, que todos elementos de un arreglo tienen el mismo tipo de dato Esto quiere decir que en el ejemplo, en el arreglo a sus elementos sólo pueden albergar valores enteros. Si quisiéramos almacenar por medio de un programa una lista de personas en donde por cada una tenemos un código, un nombre y un sueldo, para poder hacerlo tendríamos que definir un arreglo por cada uno de los datos, esto es un arreglo de enteros para el código, un arreglo de cadenas de caracteres para el nombre y uno de valor real para el sueldo. El inconveniente sería que los datos de una persona no se pueden manejar, así, como una unidad Los “registros” como se conocen en Pascal o "estructuras" como se denominan en el lenguaje C, permiten definir variables que pertenecen también a la categoría de variables estructuradas, en este sentido al declarar una estructura estaremos definiendo, como en el caso de los arreglos, un conjunto de datos, todos identificados por el mismo nombre, pero con la diferencia que cada elemento del conjunto puede ser de diferente tipo. La forma en que se diferenciará un elemento de otro del conjunto ya no se hará por medio de índices sino que se le asignará a cada elemento un nombre. Cada elemento de un registro se denominará campo. Las estructuras son un tipo primitivo de datos, su evolución ha dado lugar a las "Clases" definidas en la programación orientada a objetos (POO) e implementadas en Object Pascal y C++. Implementación de un registro De la misma manera como se implementó los arreglos, los datos de tipo registro deberán ser definidos primero como un tipo de dato para luego emplear este tipo de datos para declarar variables. La razón es la misma, esto es que la definición de un registro implica una descripción detallada y compleja, por lo que el compilador no permite una programación modular a menos que el registro se haya definido como un tipo de dato. A continuación se presenta la sintaxis en Pascal de la definición de un tipo de dato registro:
239
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
type
Nombre del tipo
=
record
Nombre del campo
:
tipo
;
end
;
De acuerdo a esta sintaxis, un tipo de dato para un arreglo se puede definir de la siguiente manera: type TipoRegistro = record codigo : Integer; nombre : String; sueldo : Real; end; Esta expresión define el tipo de dato denominado TipoRegistro el cual permitirá definir variables de tipo registro. Este tipo de dato estará compuesto por tres campos, el primero (codigo) podrá almacenar un valor entero, el segundo (nombre) guardará cadenas de caracteres y el tercero (sueldo) valores reales. Una variable de este tipo se declarará de la siguiente manera: var persona, empleado: TipoRegistro; La variable persona, de tipo registro, podrá manipular tres elementos, éstos se manejarán de la siguiente manera: persona.codigo := 7722099; persona.nombre := ‘Ana Roncal’; persona.sueldo := 3789.50; Como se puede apreciar todas las variables inmersas en el tipo de dato se identifican por el mismo nombre (persona) sin embargo cada una se diferencia de la otra por el nombre del campo. El nombre de la variable y el del campo se deben separar por un punto. Un elemento (campo) del registro como por ejemplo persona.codigo se puede manipular como cualquier otra variable sin restricción alguna, así se le podrá asignar valores directamente como en el ejemplo anterior y también desde la consola o de algún archivo, se le podrá emplear en expresiones, en fin como cualquier variable común y corriente. La desventaja con respecto a los arreglos es que no se puede generalizar el uso de los campos, esto es, que en el caso de los arreglos se puede emplear variables para manipular los índices como por ejemplo a[i] := 51;, en donde si i vale 3 nos estaremos refiriendo a a[3], y si i vale 5 a a[5]; en el caso de los registros NO se podrá hacer algo como persona.i, pensando que si i vale ‘sueldo’ nos estaremos refriendo al campo sueldo, el hacer esto dará como resultado un error de compilación. Otra ventaja del uso de los registros es que al igual que en el caso de los arreglos, se puede asignar en una sola operación todos los campos de un registro a otro como en el ejemplo siguiente: program EjemploDeAsignacionDeRegistros; type St60 = String[60]; St10 = String[10]; TRegistro = record codigo : Integer; 240
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
nombre : St60; fIngreso : St10; sueldo : Real; end; var empleado, trabajador: TRegistro; begin empleado.codigo := 775566; empleado.nombre := 'Ana Roncal'; empleado.fIngreso := '01/02/2003'; empleado.sueldo := 5680.90; // Asignación directa:
trabajador := empleado; // Manejamos el nuevo registro
writeln('Codigo : ', trabajador.codigo); writeln('Nombre : ', trabajador.nombre); writeln('Ingreso: ', trabajador.fIngreso); writeln('Sueldo : ', trabajador.sueldo:8:2); readln; end. Al ejecutar el programa obtendremos el siguiente resultado:
La asignación de registros puede hacerse también a nivel de funciones, esto quiere decir que una función puede también devolver un registro completo. El siguiente ejemplo se puede ver cómo se puede asignar todos los valores de los campos de un registro en una sola operación por medio de una función: program EjemploDeAsignacionDeRegistrosConFunciones; type St60 = String [60]; St10 = String [10]; TRegistro = record codigo : Integer; nombre : St60; fIngreso : St10; sueldo : Real; end; function asigna:TRegistro; var empleado: TRegistro; begin empleado.codigo := 775566; empleado.nombre := 'Ana Roncal'; empleado.fIngreso := '01/02/2003'; empleado.sueldo := 5680.90; asigna := empleado; // Devuelve el registro completo
end; var trabajador: TRegistro;
241
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
begin trabajador := asigna; // Asigna el registro completo writeln('Codigo : ', trabajador.codigo); writeln('Nombre : ', trabajador.nombre); writeln('Ingreso: ', trabajador.fIngreso); writeln('Sueldo : ', trabajador.sueldo:8:2); readln; end.
Situaciones complejas en la implementación de un registro Una de las características más importante de la implementación de un registro es la que un campo puede ser definido empleando cualquier tipo de dato. Esto quiere decir que un campo puede ser un tipo de dato escalar (Integer, Real, Char, Boolean, etc.) como estructurado (String, Array, Record, etc.) y por lo tanto no hay límites. Por esta razón se deben analizar los diferentes casos que se puedan presentar a fin que se aprecie cómo se puedan resolver. Registros anidados: Esta situación se presenta cuando existe un tipo de dato de tipo registro y este tipo se emplea para definir el campo de otro tipo de dato registro. El siguiente ejemplo muestra esta situación: program registrosAnidados type St60 = String [60]; TFecha = record dd : Integer; // día mm : Integer; // mes aa : Integer; // año end; TRegistro = record codigo : Integer; nombre : St60; fIngreso : TFecha; sueldo : Real; end; var trabajador: TRegistro; Aquí podemos apreciar que el campo fIngreso (fecha de ingreso) está definido como un tipo de dato registro (TFecha) esto ocasionará que cuando se desee asignar valores a este campo, se le tendrá que manejar precisamente como un registro, esto es: empleado.codigo := 775566; empleado.nombre := 'Ana Roncal'; empleado.fIngreso.dd := 1; empleado.fIngreso.mm := 2; empleado.fIngreso.aa := 2003; empleado.sueldo := 5680.90; 242
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
De esa misma forma, se pueden incrementar los niveles de anidación de modo que un campo sea definido con otro tipo registro que a su vez tiene campos definidos como registro. En esos casos sólo habrá que incrementar el nombre de la variable, recordando que hay que emplear el punto (.) para separar cada nivel. Campos de tipo arreglo y arreglos de tipo registro: Aunque esto parezca un trabalenguas estas situaciones se pueden presentar por lo que la nomenclatura que se emplee será muy importante analizar para no cometer errores. El primer caso que analizaremos es el del campo como arreglo en el siguiente ejemplo: program EjemploDeCamposComoArreglos; type St60 = String [60]; TVector = array [1..5] of Integer; TAlumno = record codigo : Integer; nombre : St60; curso : St60; nota : TVector; sueldo : Real; end; var alumno: TAlumno; En este caso hemos defiendo un campo denominado nota, que guardará las notas parciales de un alumno en un curso determinado. Entonces para poder asignar por ejemplo la tercera nota del curso a la variable deberemos tomar en cuenta que el arreglo es el campo nota y no la variable alumno por lo que el índice debe acompañar al campo y no al registro, esto es: empleado.nota[3] := 17; // El índice se coloca a la derecha del campo. Por otro lado, qué pasa si queremos definir un arreglo en el que cada elemento sea un registro, por ejemplo un arreglo de tipo TAlumno, veamos ese caso: program EjemploDeArregloDeRegistros; type St60 = String [60]; TVector = array [1..5] of Integer; TAlumno = record codigo : Integer; nombre : St60; curso : St60; nota : TVector; sueldo : Real; end; TVectosAl = array [1..50] of TAlumno; //Tipo arreglo de registros var alumno: TVectorAl; Aquí se tiene que tener mucho cuidado al manejar los elementos, en primer lugar hay que entender que alumno es un vector y que los campos codigo o nombre no lo son, de modo 243
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
que si se quisiera asignar los datos al séptimo elemento del arreglo, la forma de hacerlo será de la siguiente manera: alumno[7].codigo := 775566; // El índice se coloca a la derecha del registro. alumno[7].nombre := 'Ana Roncal'; Sin embargo si quisiéramos asignar la nota 3 al séptimo alumno deberemos hacer: alumno[7].nota[3]:= 17; // El índice se coloca a la derecha del registro y también // a la derecha del campo porque cada uno es un arreglo
Aplicaciones que emplean registros Veamos ahora alguna aplicación que permita manejar registros. Ordenar un arreglo de registros En este ejemplo veremos cómo organizar la información en un arreglo de registros en lugar de un grupo de arreglos como se hizo en capítulos anteriores, aquí se podrá apreciar las ventajas que esto trae como también algunos detalles no muy prácticos, como por ejemplo se verá que es una ventaja a la hora de ordenar los datos que se puede manejar toda la información de una persona como una unidad, esto hará que se intercambien los datos de manera más sencilla; sin embargo veremos una desventaja a la hora de leer e imprimir los datos porque los identificadores son más grandes. Los datos serán leídos desde un archivo de textos como el que se mostrará a continuación. Se contempla que la información del archivo corresponde a la de alumnos de un curso, esto es que se tiene su código, nombre y notas (6). El programa leerá estos datos y los almacenará en registros, luego calculará los promedios correspondientes, que también serán almacenados en el registro y finalmente ordenará estos datos en función al promedio obtenido y los mostrará en la pantalla: Datos:
Programa:
19992345 Juan Lopez 12 14 10 8 12 10 20001010 Maria Ruiz 10 12 11 9 9 5 20101234 Ana Roncal 15 16 12 14 18 15 20002324 Pedro Sanchez 14 15 18 12 13 16 20020107 Paula Gomez 17 19 20 12 15 16 19971313 Carlos Castro 2 5 9 11 10 6 20041003 Alexandra Neyra 18 15 17 16 18 13 …
program ordenaRegistros; const MAXAL = 100; type St10 = String[10]; St60 = String[60]; 244
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
TVectorN = array TVectorN = array [ [11.. ..66]of Integer Integer;; TReg = TReg = record codigo : codigo : St10 St10;; nombre : nombre : St60 St60;; nota : nota : TVectorN TVectorN;; prom : prom : Real Real;; end;; end TVectorA = TVectorA = array array [ [11.. ..MAXAL MAXAL]] of TReg TReg;; var alumno alumno:: TVectorA TVectorA;; numAlum:: Integer numAlum Integer;; begin leeDatos((Alumno leeDatos Alumno,,numAlum numAlum); ); calculaPromedios((Alumno calculaPromedios Alumno,,numAlum numAlum); ); ordenaPorNota((Alumno ordenaPorNota Alumno,,numAlum numAlum); ); imprime((Alumno imprime Alumno,,numAlum numAlum); ); readln end.. end procedure leeDatos leeDatos((var alumno alumno::TVectorA TVectorA;; var numAlum numAlum:: Integer Integer); ); var arch arch::Text Text;; i: Integer Integer;; begin assign((arch assign arch,,'ord-reg.dat' 'ord-reg.dat'); ); reset((arch reset arch); ); numAlum := 0; while not eof eof((arch arch)) do begin inc((numAlum inc numAlum); ); readln((arch readln arch,, alumno alumno[[numAlum numAlum]. ].codigo codigo); ); readln((arch readln arch,, alumno alumno[[numAlum numAlum]. ].nombre nombre); ); for i:= :=11 to 6 do read read((arch ,alumno alumno[[numAlum numAlum]. ].nota nota[[i]); readln((arch readln arch); ); end;; end close((arch close arch); ); end;; end procedure calculaPromedios calculaPromedios((var alumno alumno::TVectorA TVectorA;; numAlum numAlum:: Integer Integer); ); var i: Integer Integer;; begin for i := 1 to numAlum do alumno[[i]. alumno ].prom prom := := promedio promedio((alumno alumno[[i]. ].nota nota); ); // Se envía el arreglo end;; end function promedio promedio((var nota nota::TVectorN TVectorN): ):real real;; var suma suma:: real real;; // Se recibe el arreglo i: Integer Integer;; begin 245
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
suma := 0; suma := for i := 1 to 6 do suma suma := := suma suma + + nota nota[[i]; promedio := promedio := suma suma / / 6; end;; end procedure ordenaPorNota ordenaPorNota((var alumno alumno:: TVectorA TVectorA;; numAlum numAlum:: Integer Integer); ); var i, j j:: Integer Integer;; aux:: TReg aux TReg;; begin for i := 1 to numAlum numAlum-1 -1 do for j for j := := i+1 to numAlum do if alumno alumno[[i]. ].prom prom < < alumno alumno[ j]. [j].prom prom then begin aux := aux := alumno alumno[[i]; // Se maneja el registro alumno[[i] := alumno alumno alumno[ j]; [j]; // como una unidad alumno[ j] alumno [j] := aux aux;; end;; end end;; end procedure imprime imprime((var alumno alumno:: TVectorA TVectorA;; numAlum numAlum:: Integer Integer); ); var i, j j:: Integer Integer;; begin for i := 1 to numAlum do begin write((alumno write alumno[[i]. ].codigo codigo::10 10,, alumno alumno[[i]. ].nombre nombre::20 20); ); for j for j := := 1 to 6 do write write((alumno alumno[[i]. ].nota nota[ j]: [j]:33); writeln((alumno writeln alumno[[i]. ].prom prom::8:2); end end;; end El resultado del programa será como sigue:
Campeonato de futbol Se desea tener un programa que dada la lista de equipos que participan en un campeonato de fútbol y los resultados obtenidos en los partidos en la primera etapa, permita obtener un reporte que muestre la tabla de posiciones de cada grupo, así como los dos equipos de cada grupo que se clasifican a la siguiente etapa. Para esto se tiene un archivo de textos en el cual aparecen primero los nombres de los equipos que participan separados por grupos, y luego aparecen los resultados en el orden en que se jugaron, como se muestra en el ejemplo ejemplo siguiente:
246
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo Archivo: futbol.txt Copa Mundial 2010 Grupo A 01 Italia 02 Austria 03 Marruecos 04 Estados Unidos Grupo B … Grupo J 01 Inglaterra 02 República Checa 03 Perú 04 Haití ***Fin grupos*** Partido Resultado 01 0 1 02 2 2 03 3 0 … 51 4 1 52 0 0 ...
Equipos Argentina – Camboya Polonia – Uruguay Italia – Marruecos Perú – Inglaterra Colombia – Finlandia
El reporte deberá ser como a continuación se detalla: Tabla de posiciones Copa M undial 2010 Grupo A Equipo PJ PG Italia 3 2 Austria 3 1 Marruecos 3 0 Estados Unidos 3 2 Grupo B …
PE 1 1 0 0
PP 0 1 3 1
GF 6 1 2 4
GC 2 3 5 1
DG 4 -2 -3 3
Puntos 7 4 0 6
Clasifica Clasifica
Donde: PJ = Partidos jugados PP = Partidos perdidos DG= Diferencia de goles
PG = Partidos ganados GF = Goles a favor
PE = Partidos empatados GC = Goles en contra
Para el puntaje que se otorga a cada partido considera 3 puntos por partido ganado, 1 por partido empatado y 0 por partido perdido. Se clasifican los equipos con mayor puntaje, si igualan en puntaje el que tiene mayor diferencia de goles y si ambos tienen igual diferencia de goles, el que metió más goles. Solución: El problema al que nos enfrentamos es complejo por la cantidad de datos que hay que procesar y mantener almacenados, por eso es importante que manejemos una adecuada estructura de datos, de modo que evitemos duplicar la información innecesariamente y que las estructuras no sean demasiado grandes que dificulten su manejo. Emplear una sola estructura en la que se almacene toda información puede volverse inmanejable ya que se trata de mucha información y porque de ser así alguno de los procesos se puede volver muy complejo, por esta razón emplearemos dos estructuras de datos en la solución de este problema, una de ellas guardará los nombres de los países, y la otra las tablas de posiciones. Sin embargo debido a que son dos estructuras, se debe considerar elementos que enlacen una estructura con la otra, por eso a la primera estructura se le adicionarán dos campos, uno que guarde el grupo al que se le ha ubicado el país y el otro su posición dentro del grupo. A la estructura que almacene las tablas de posiciones se le añadirá un campo que guarde la posición del equipo en la estructura de países, de este modo cuando estemos trabajando con una estructura, podremos emplear los datos de la otra sin tener que hacer ha cer búsquedas innecesarias. A continuación presentamos las estructuras estas estructuras de datos: 247
const // Los grupos irán de la MAXGRUPOS = MAXGRUPOS = 'Z' 'Z';;
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo 'A' a 'Z' como máximo
// Cada grupo tendrá máximo 4 equipos
MAXEQXGR = 4;
// Cantidad de equipos que pueden participar
MAXEQUIPOS = (ord MAXEQUIPOS = (ord((MAXGRUPOS MAXGRUPOS)) - ord ord(('A' 'A')) + 1) * MAXEQXGR MAXEQXGR;; type Str30 Str30 = = String String[[30 30]; ]; TRegEquipo = TRegEquipo = record pais : Str30 Str30;; grupo : grupo : Char Char;; orden : orden : Integer Integer;; //Posisción en el grupo end;; end TArrEquipos = TArrEquipos = array array [ [11 .. MAXEQUIPOS MAXEQUIPOS]] of TRegEquipo TRegEquipo;; TRegTabla = TRegTabla = record posEq : posEq : Integer Integer;; pJ : pJ : Integer Integer;; pG : pG : Integer Integer;; pE : pE : Integer Integer;; pP : pP : Integer Integer;; gF : gF : Integer Integer;; gC : gC : Integer Integer;; dG : dG : INteger INteger;; pts:: Integer pts Integer;; end;; end // Estructura que contiene un grupo
TArrGrupo = TArrGrupo = array array [ [11.. ..MAXEQXGR MAXEQXGR]] of TRegTabla TRegTabla;; // Estructura que contiene todos los grupos
TArrCampeonato = TArrCampeonato = array array ['A'.. ['A'.. MAXGRUPOS MAXGRUPOS]] of TArrGrupo TArrGrupo;; // Arreglo que servirá para el ordenamiento
TArrIndices = array TArrIndices = array [ [11.. ..MAXEQXGR MAXEQXGR]] of Integer Integer;; El programa principal para este problema será muy simple, esto porque conforme se lean los datos habrá que ir actualizando las tablas, luego será el procedimiento de lectura el que lleve toda la carga del proceso. var paises paises:: TArrPaises TArrPaises;; grupo:: TArrCampeonato grupo TArrCampeonato;; numPaises:: Integer numPaises Integer;; ultipoGrupo:: Char ultipoGrupo Char;; nombMundial:: String nombMundial String;; begin leerDatosCrearTabla((paises leerDatosCrearTabla paises,, numPaises numPaises,, grupo grupo,, ultimoGrupo ultimoGrupo,, nombMundial nombMundial); ); imprimirTablas((paises imprimirTablas paises,, numPaises numPaises,, grupo grupo,, ultimoGrupo ultimoGrupo,, nombMundial nombMundial); ); end.. end La lectura de datos, conforme está estructurado el archivo, se tendrá que dividir en dos partes. La primera se encargará de leer los nombres de los países, aquí se deberá 248
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
registrar también el grupo y la ubicación del país en la estructura que se ha denominado “paises paises”” en el programa, y la posición del país leído en esa estructura deberá ser registrada también en la estructura “grupo “grupo”, ”, de modo que se pueda ir de uno a otro directamente sin hacer necesariamente búsquedas. La segunda parte sólo leerá los datos y actualizará las tablas de puntajes. El proceso es el siguiente: procedure leerDatosCrearTabla leerDatosCrearTabla ( (var var paises paises:: TArrPaises TArrPaises;; var numPaises numPaises:: Integer Integer;; var grupo grupo:: TArrCampeonato TArrCampeonato;; var ultimoGrupo ultimoGrupo:: Char Char;; var nombMundial nombMundial:: String String); ); var arch arch:: Text Text;; nombArch,, gr nombArch gr,, pais pais,, pais1 pais1,, pais2 pais2:: String String;; fin:: Boolean fin Boolean;; p, num num,, partido partido,, gol1 gol1,, gol2 gol2:: Integer Integer;; begin numPaises := 0; write(('Ingrese el nombre del archivo: '); write ' ); readln((nombArch readln nombArch); ); assign((arch assign arch,, nombArch nombArch); ); reset reset((arch arch); ); // Leemos el nombre del mundial, se requiere en el reporte
readln((arch readln arch,, nombMundial nombMundial); ); // 1ra parte: Leemos los paises
fin := false fin := false;; while not eof eof((arch arch)) and not fin do begin // Leemos la identificación del grupo
readln(arch readln( arch,, gr gr); ); if gr gr = = '***Fin grupos***' then fin fin := := true else begin ultimoGrupo := ultimoGrupo := obtenerGrupo obtenerGrupo((gr gr); ); // Leemos los paises del grupo
for p := 1 to MAXEQXGR do begin inc((numPaises inc numPaises); ); readln((arch readln arch,, num num,, pais pais); ); limpiarCadena((pais limpiarCadena pais); ); paises[[numPaises paises numPaises]. ].pais pais := pais pais;; paises[[numPaises paises numPaises]. ].grupo grupo := ultimoGrupo ultimoGrupo;; paises[[numPaises paises numPaises]. ].orden orden := num num;; // Colocamos El enlace en “grupo” e inicializamos los demás campos
inicializarRegistroG(grupo inicializarRegistroG( grupo,, ultimoGrupo ultimoGrupo,, num num,, numPaises numPaises); ); end;; end end;; end end;; end
249
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
// 2da. Parte: Leemos los resultados readln(arch); // Saltamos la línea con títulos
while not eof(arch)do begin readln(arch, partido, gol1, gol2, pais); separarPaises (pais, pais1, pais2); actualizarTabla ( pais1, gol1, gol2, paises, numPaises, grupo, ultimoGrupo); actualizarTabla ( pais2, gol2, gol1, paises, numPaises, grupo, ultimoGrupo); // Observar que no es necesario definir dos procedimientos para actualizar las tablas
end; end;
La primera parte de este procedimiento se apoyará en tres procesos: “obtenerGrupo” que se encargará de extraer la letra del grupo de la cadena leída (‘Grupo A’), “limpiarCadena” que eliminará los espacios en blanco en exceso que pueda tener la cadena al inicio, al final o los intermedios, en este último caso se trata de dejar sólo un espacio entre las palabras para hacer más fácil los procesos con las cadenas, y finalmente el procedimiento “inicializarRegistroG” que pondrá en cero todos los campos (pJ, pG, pE, etc.) y además registrará la posición del país en el otro arreglo. function obtenerGrupo(grupo: String):Char; begin limpiarCadena(grupo); delete(grupo, 1, 6); // Borramos la cadena 'Grupo ' obtenerGrupo := grupo[1]; end; procedure inicializarRegistroG(var grupo: TArrCampeonato; gr: Char; n, numPais: Integer); begin grupo[gr][n].posEq := numPais; grupo[gr][n].pJ := 0; grupo[gr][n].pG := 0; grupo[gr][n].pE := 0; grupo[gr][n].pP := 0; grupo[gr][n].gF := 0; grupo[gr][n].gC := 0; grupo[gr][n].dG := 0; grupo[gr][n].pts := 0; end; procedure limpiarCadena (var cadena: String); var p: Integer; begin // Borramos blancos iniciales
while cadena[1] = ' ' do delete(cadena, 1, 1); // Borramos blancos finales
while cadena[length(cadena)] = ' ' do delete(cadena, length(cadena), 1); 250
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
// Borramos blancos sobrantes
repeat p := pos(' ', cadena); // Buscamos dos blancos y borramos uno if p <> 0 then delete(cadena, p, 1); until p = 0; end; La segunda parte del proceso de lectura se apoyará en dos procedimientos: “separarPaises” ya que a la hora de leer, los dos países se leen juntos, y “actualizarTabla”, este procedimiento se ha diseñado de modo tal que no se tenga que repetir el código ya que se deben procesar dos paises. Lo que se ha hecho en este módulo es que el procedimiento recibirá sólo tres datos, el nombre del país, los goles que anotó y los goles que le anotaron, de esta forma el procedimiento se llama dos veces. procedure separarPaises(equipos: String; var pais1, pais2: String); var p: Integer; begin limpiarCadena(equipos); p := pos('-', equipos); pais1 := copy(equipos, 1, p - 2); pais2 := copy(equipos, p + 2, 255); end; procedure actualizarTabla(pais:String; golF, golC: Integer; var paises: TArrPaises; numPaises: Integer; var grupo: TArrCampeonato; ultimoGrupo: Char); var p, num: Integer; gr: Char; begin p := buscar(pais, paises, numPaises); gr := paises[p].grupo; num := paises[p].orden; inc(grupo[gr][num].pJ); if golF = golC then inc(grupo[gr][num].pE) else if golF > golC then inc(grupo[gr][num].pG) else inc(grupo[gr][num].pP); inc(grupo[gr][num].gF, golF); inc(grupo[gr][num].gC, golC); grupo[gr][num].dG := grupo[gr][num].gF - grupo[gr][num].gC; grupo[gr][num].pts := 3*grupo[gr][num].pG + grupo[gr][num].pE; end; La segunda parte de este programa corresponde a la impresión de las tablas de posiciones, aquí se presenta una dificultad, que es que las tablas de posiciones deben apareces ordenadas por el puntaje y mostrar los clasificados. En este caso, a modo de presentar otras formas de ordenar, se ha decidido mostrar una forma diferente de clasificar la información, ésta se denomina “ordenación por índices”. De lo que trata este método es de mostrar los datos ordenados pero sin alterar la ubicación original de los 251
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
datos. La técnica es muy simple, se crea un arreglo de enteros, paralelo a los datos, y en él se colocan las posiciones (índices) que tienen los datos pero en el orden que se espera que aparezcan, el ejemplo siguiente muestra cómo se consigue esto. Dado u arreglo como el que se muestra a continuación: Datos Maria Pedro Jose Daniel Ana Valentina Naomi Carlos
1 2 3 4 5 6 7 8
Si se quiere imprimir los datos ordenados alfabéticamente, se debe creaa un arreglo (de índices) de la siguiente manera: Índices
1 5
2 8
3 4
4 3
5 1
6 7
7 2
8 6
Aquí se aprecia que en el arreglo de índices, en la primera posición se ha colocado el valor de 5 y esto es porque en el arreglo de datos, en la quinta posición se encuentra ‘Ana’ que es el primer elemento del arreglo que debería aparecer al imprimir los datos ordenados, en la segunda posición se encuentre el 8, que corresponde a ‘Carlos’ el segundo que debería salir, y así sucesivamente. Observe que si en un programa escribiéramos: for i := 1 to n do writeln(datos[i]); obtendríamos un reporte como el de la tabla A, pero si escribimos: for i := 1 to n do writeln(datos[indice[i]]); el reportes sería como en la tabla B: Tabla A Maria Pedro Jose Daniel Ana Valentina Naomi Carlos
Tabla B Ana Carlos Daniel Jose Maria Naomi Pedro Valentina
En la tabla B se puede ver que los nombres salen ordenados sin embargo no se ha modificado la posición original de los datos. La pregunta que nos hacemos ahora es ¿cómo construimos esa tabla de índices? La forma de conseguirlo no es muy compleja, lo que se hace es, como se indicó, definir un arreglo de enteros denominado de índices, en el que inicialmente se colocan valores consecutivos, empezando del 1 hasta el número de elementos del arreglo original. Luego se procede a ordenar el arreglo, esto se hace empleando cualquier método de ordenación, en esta caso se empleará el método de intercambio que se ha utilizado en todo este texto, pero empleando tanto el arreglo de datos como el arreglo de índices. Lo que se cambia en este proceso es que cuando se hace la pregunta para ver si un elemento está desordenado con respecto a otro se emplea el arreglo de datos pero a la hora de intercambiarlos, los que cambiarán de posición serán los índices y no los datos. El código para realizar la impresión de las tablas se muestra a continuación:
252
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
procedure imprimirTablas ( paises: TArrPaises; numPaises: Integer; grupo: TArrCampeonato; ultimoGrupo: Char; nombMundial: String); var p, i: Integer; g: Char; ind: TArrIndices; begin writeln('Tabla de posiciones: ', nombMundial); for g:= 'A' to ultimoGrupo do begin writeln('Grupo ', g, ':'); writeln('Equipo PJ PG PE PP GF GC DG Puntos'); determinarIndices(grupo[g],ind); for p:= 1 to MAXEQXGR do begin write(paises[grupo[g][ind[p]].posEq].pais); for i := 1 to 20-length(paises[grupo[g][ind[p]].posEq].pais) do write(' '); // Esto para la alineación correcta de los datos write ( grupo[g][ind[p]].pJ:4, grupo[g][ind[p]].pG:4, grupo[g][ind[p]].pE:4, grupo[g][ind[p]].pP:4, grupo[g][ind[p]].gF:4, grupo[g][ind[p]].gC:4, grupo[g][ind[p]].dG:4, grupo[g][ind[p]].pts:4); if p <= 2 then writeln(' Clasifica') else writeln; end; writeln; end; end; A continuación la creación de la tabla de índices: procedure determinarIndices(pais: TArrGrupo; var ind: TArrIndices); var p, i, j, auxInd: Integer; begin // Llenamos la tabla de índices con valores consecutivos
for p := 1 to MAXEQXGR do ind[p]:= p;
// Empleamos el método de intercambio para ordenar los datos
for i := 1 to MAXEQXGR - 1 do for j := i + 1 to MAXEQXGR do
// Comparamos empleando los datos pero a través de los índices
if (pais[ind[i]].pts < pais[ind[ j]].pts) or ((pais[ind[i]].pts = pais[ind[ j]].pts) and (pais[ind[i]].dG < pais[ind[ j]].dG)) or ((pais[ind[i]].pts = pais[ind[ j]].pts) and (pais[ind[i]].dG = pais[ind[ j]].dG) and (pais[ind[i]].gF < pais[ind[ j]].gF)) then begin // intercambiamos sólo los índices
253
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
auxInd := ind[i]; ind[i] := ind[ j]; ind[ j] := auxInd; end;
end; La ejecución del programa dará una respuesta similar a la siguiente:
254
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
CAPÍTULO 11: Archivos binarios Definición Un archivo es una colección de datos que se encuentran almacenados de manera permanente en algún dispositivo externo del computador (memoria secundaria), como el disco duro, dispositivo o memoria USB, CDs, etc. En realidad todo lo que se almacena en el computador se considera un archivo, así por ejemplo si escribimos una carta en algún procesador de palabras y luego la guardamos, esta carta se constituye en un archivo, si escribimos un programa en un editor de textos y lo guardamos, éste también es un archivo, de igual manera el programa ejecutable (con extensión .exe) también constituye un archivo, y así una fotografía digital, un video en un CD, una hoja de cálculo también es un archivo. En otros capítulos se ha trabajado con archivos de texto, se ha visto la manera tan práctica que representa manejar un archivo de textos para el ingreso y la salida de datos y también se han apreciado las limitaciones que tienen estos archivos, sin embargo los archivos de textos son sólo un tipo de archivo. Lo que estudiaremos en este capítulo es otro tipo de archivo, los archivos binarios, que sin alejarse de la definición de lo que es un archivo, por la forma en que se almacenan los datos difiere sustancialmente de lo que es un archivo de textos y por lo tanto la manera de procesarlos también será diferente. Formas en las que se puede almacenar información en un archivo Cuando se habla de un archivo desde el punto de vista de la programación, no es del todo correcto hablar de "tipos de archivos". Esto porque en realidad un archivo es sólo una colección de bytes consecutivos almacenados, y en ese sentido todos los archivos son lo mismo para el computador. Lo que hace la diferencia es el formato cómo se almacena la información; es así que podemos distinguir dos modelos diferentes, los conocidos como "archivos de texto" y los denominados "archivos binarios". En los archivos de texto, la información sufre una transformación. Cuando se guarda un dato en un archivo de texto, el valor es convertido a una cadena de caracteres antes de almacenarse en el archivo. Recuerde que los datos en un programa se almacenan en variables y éstas son posiciones de memoria, pues bien, como sabemos en la memoria del computador la información (por ejemplo un número) se guarda en una representación binaria; sin embargo cuando abrimos un archivo de texto no vemos esta representación binaria sino el número como lo entendemos, en otras palabras una secuencia de dígitos. Entienda entonces que cuando en un programa se hace un asignación de la forma a := 93751; donde “a” es definida de tipo Integer, lo que se hace es colocar en la posición de memoria relacionada a la variable a, la representación binaria del número, esto es el valor de 0011 0111 0110 1110 0000 0001 0000 0000 (representación de 4 bytes), luego cuando se envía el contenido de esta variable al archivo de textos, se toma esta representación binaria, se transforma en la cadena ‘93751’ y cada caracteres que la conforman es colocado en el archivo. Observe que en la variable se emplean 4 bytes para almacenar el número sin embargo al archivo se envían 5 caracteres o bytes; si el 255
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
número hubiera sido 751, al archivo sólo se enviarían 3 bytes y si fuera 193751 se enviarían 6 bytes. Cuando se hace el proceso inverso, es decir cuando se lee información del archivo, el sistema toma uno a uno los caracteres del archivo los convierte a una representación binaria y finalmente lo almacena en la variable. En el ejemplo anterior, se toman los 5 caracteres y se transforman en 4 bytes. En los archivos binarios, la información no se transforma cuando es colocada en el archivo. La representación binaria del dato, tal como se encuentra en la memoria asignada a la variable, es llevada y almacenada en el archivo. Esta es una diferencia muy fuerte con respecto a los archivos de texto que influirá de manera significativa en la forma cómo se accederá a la información del archivo. Una de las cosas que podemos apreciar en esta forma de almacenar los datos en el archivo es que cuando llevamos un valor, por ejemplo de una variable de tipo Integer, serán los 4 bytes que conforman la variable lo que se enviarán al archivo, sin importar el número de cifras que tenga el valor. Esto es, si el número fuera 93751, la información almacenada en la variable que será llevada al archivo será 0011 0111 0110 1110 0000 0001 0000 0000, si fuera 193751 lo que se almacenará será 1101 0111 1111 0100 0000 0010 0000 0000, y si el número fuera 751 lo que se almacenará en el archivo será 1110 1111 0000 0010 0000 0000 0000 0000, 4 bytes en todos los casos.
Diferencias funcionales entre un archivo de textos y uno binario La forma cómo se almacena la información en los archivos afectará en gran medida la forma cómo se accederán a ellos, a continuación presentaremos estos aspectos: Separación de los datos: Dado que los datos en un archivo de texto son secuencias de caracteres y por lo tanto la cantidad de bytes para almacenar un valor dependerá de la cantidad de cifras que tiene el valor, se deben que definir separadores (caracteres especiales) que permitan al sistema saber dónde empieza y dónde termina el dato. Estos separadores son el espacio en blanco (' '), el tabulador (#9) y el cambio de línea ( #13, #10). Por otro lado, en los archivos binarios lo que se guarda es la misma secuencia de bytes que tiene la representación numérica del dato en la memoria, y que la cantidad de bytes que se guarda en el archivo depende del tipo de dato y no de la cantidad de cifras que tiene el número. Por esta razón, los archivos binarios no requieren de separadores; si se guarda en el archivo un valor entero (Integer), para recuperarlo sólo se requiere extraer del archivo 4 bytes para tener lo número completo, luego los siguientes 4 bytes que se encuentren en el archivo pueden perfectamente corresponder al siguiente dato sin la necesidad de separadores. En un archivo de textos la única forma de detectar que se culminó la lectura de un valor entero será cuando se detecte el separador. A continuación se muestran estas diferencias: var a, b, c: Integer; begin a := 735; b := 29; c := 173923; …
En memoria:
a
11011111 00000010 00000000 00000000
b
00011101 00000000 00000000 00000000
c
01100011 10100111 10000000 00000000
256
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Al enviar los datos a un archivo de textos, la información quedará de la siguiente manera:
‘7’
bytes o caracteres
‘3’ ‘5’ ‘ ’ ‘2 ’ ‘9’ ‘ ’ ‘1 ’ ‘7’ ‘3’ ‘9’ ‘2’ ‘3’ ‘…’
separadores
Al enviar los datos a un archivo binario, la información quedará así: 735
29
173923
1101 1111 0000 0010 0000 0000 0000 0000 0001 1101 0000 0000 0000 0000 0000 0000 0110 0011 1010 0111 1000 0000 0000 0000
bytes
Acceso a los datos: En un archivo de texto, la única forma de acceder a los datos es de manera secuencial, la razón es muy simple, si los dos primeros datos de un archivo de textos no son relevantes pero el tercero sí, para poder leer el tercer dato se requiere leer antes los otros dos, esto porque no hay manera de saber la ubicación del tercer dato para poder colocarse allí y leerlo, se debe leer el primer dato y una vez que se detecte el separador se sabrá que se ha leído el primer valor, de igual manera pera el segundo, y recién allí se sabrá que se está listo para leer el tercer dato. Por lo tanto, en un archivo de textos no se puede leer un dato sin antes haber leído los anteriores, a esto se le denomina “Acceso Secuencial”. La forma cómo se almacenó los datos limita, pues, esta acción. En los archivos binarios, como el espacio almacenado por los datos depende del tipo de dato y no del número de cifras, es muy fácil poder calcular la posición en que se ubica el dato que nos interesa obtener, por ejemplo si tenemos almacenados en un archivo una serie de números enteros y queremos extraer el tercer valor, sólo debemos calcular cuántos bytes hay desde el inicio del archivo hasta el inicio del dato. En este caso hay dos valores antes del dato, que multiplicado por 4 bytes, que es lo que ocupa un entero en memoria, nos dan 8 bytes. Entonces no tendremos que leer los dos datos que se encuentran antes del que nos interesa, sólo debemos desplazarnos 8 bytes desde el inicio del archivo para ubicarnos en el dato que nos interesa y leerlo sin tocar los anteriores. Esta forma de acceder a los datos de un archivo se denomina "Acceso Directo". En resumen se puede afirmar que en los archivos de textos los datos se acceden de manera secuencial los datos, sin embargo en los archivos binarios los datos se pueden acceder de manera secuencial, pero también de manera directa. A continuación se presenta gráficamente la manera cómo se extrae la información de los archivos: 257
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
En los archivos de textos, la única forma de acceder la información es de manera secuencial:
a
Al detectar el separador, se da por terminada la lectura del primer dato
‘5’
11011111 00000010 00000000 00000000
3°
‘3’ ‘7’
4°
2°
7° Al detectar el separador, se da por terminada la lectura del egundo dato y se está listo recién para leer el tercero
1°
‘735’ ‘7’
‘3’ ‘5’ ‘ ’ ‘2 ’ ‘9’ ‘ ’ ‘1 ’ ‘7’ ‘3’ ‘9’ ‘2’ ‘3’ ‘…’ ‘2’
5°
‘9’
6°
En los archivos binarios, se puede calcular la posición del dato en el archivo, desplazar el indicador del archivo y luego proceder con su lectura sin haber leído los datos que lo preceden: Al abrirlo, el indicador del archivo se coloca al inicio. 1°
Se calcula la posición del dato en el archivo (2 x 4 bytes = 8 bytes) y e desplaza el indicador del archivo 2°
3° Ya se está listo para leer el dato sin haber leído los anteriores.
1101 1111 0000 0010 0000 0000 0000 0000 0001 1101 0000 0000 0000 0000 0000 0000 0110 0011 1010 0111 1000 0000 0000 0000
735
29
173923
Actualización de datos: La actualización de datos en un archivo se refiere a modificar la información que se tiene guardada en el archivo. Para poder realizar esto se requiere de tres pasos, primero leer el dato que se quiere modificar del archivo, luego se tiene que actualizar o modificar el dato en memoria y finalmente volverlo a guardar en el archivo. Una vez leído un dato de un archivo, el indicador del archivo está listo para leer el siguiente valor, luego está colocado en el byte siguiente al dato leído. Para poder modificar el dato leído y finalmente guardar la modificación en el archivo, se tiene que retroceder el indicador del archivo y colocarlo al inicio del dato leído, luego se debe proceder a guardar el dato en el archivo a partir de esa posición. Aquí es donde se presenta un problema en los archivos de textos, imagínese que el dato leído tiene dos cifras y que luego de su modificación el número quedara con 5 dígitos. Resultaría que los dos primeros dígitos del número ocuparían las dos posiciones originales del número, el tercero se colocaría en la posición del separador y las dos últimas se colocarían en las posiciones de las dos primeras cifras del siguiente dato, el resultado sería un daño irreparable al archivo. Este problema no se presenta con los archivos binarios por lo que ya se ha comentado, la información en el archivo no depende del número de cifras, luego no importa si el número 258
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
crece o se reduce, siempre ocupará el mismo espacio. Por lo tanto, en este tipo de archivo la actualización de datos se puede realizar si complicación alguna. Son por estas razones que algunos lenguajes de programación como el Pascal no permiten realizar actualizaciones en los archivos de texto, sin embargo en el caso de lenguajes como el C y el C++ esto sí se permite pero dejan al programador la responsabilidad de controlar los errores que se puedan producir. A continuación se ilustra este proceso: En los archivos de textos, las acciones que se deberían seguir para actualizar un dato serían las siguientes: 1° Luego detectar el dato, se
b
debe proceder a leerlo
00011101 00000000 00000000 00000000
‘7’
‘29’
‘3’ ‘5’ ‘ ’ ‘2 ’ ‘9’ ‘ ’ ‘1’ ‘7’ ‘3’ ‘9’ ‘2’ ‘3’ ‘…’ ‘2’ ‘9’
2° Luego se debe procede a
b := b * 627; // b 18183
modificar el dato
b 00000111 01000111 00000000 00000000
3° A continuación se debe, de alguna forma,
regresar el indicador del archivo para colocarse nuevamente en la posición del dato leído
4
‘7’
‘3’ ‘5’ ‘ ’ ‘2 ’ ‘9’ ‘ ’ ‘1 ’ ‘7’ ‘3’ ‘9’ ‘2’ ‘3’ ‘…’
Finalmente se debe proceder a grabar el dato modificado en el archivo
b 00000111 01000111 00000000 00000000
‘3 ‘8
‘18183
‘1 ‘8 ‘1
‘7’
‘3’ ‘5’ ‘ ’ ‘1 ‘8 ‘1 ‘8 ‘3 ‘3’ ‘9’ ‘2’ ‘3’ ‘…’
Como se aprecia el dato que ingresa destruye el iguiente dato del archivo.
259
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
En los archivos binarios las acciones son similares, pero aquí no hay peligro de estropear el archivo: 1° Luego colocar el indicador del archivo en el dato, se debe proceder a leerlo
1101 1111 0000 0010 0000 0000 0000 0000 0001 1101 0000 0000 0000 0000 0000 0000 0110 0011 1010 0111 1000 0000 0000 0000
735
173923
29
b 00011101 00000000 00000000 00000000
2° Luego se debe procede a modificar el dato, igual como se hizo con el archivo de texto
b := b * 627
b
// b 18183
00000111 01000111 00000000 00000000
3° Finalmente se coloca nuevamente el indicador del archivo en la posición del segundo dato y e graba el dato en el archivo
b 00000111 01000111 00000000 00000000
1101 1111 0000 0010 0000 0000 0000 0000 0000 0111 0100 0111 0000 0000 0000 0000 0110 0011 1010 0111 1000 0000 0000 0000
735
18183
173923
El dato que ingresa no altera los otros datos del archivo
Funciones y procedimientos elementales que manejan archivos binarios: Las funciones y procedimientos para manejar los archivos binarios en Pascal no difieren mucho de los empleados para los archivos de texto, sin embargo en muchos de los casos se tendrá que tomar muy en cuenta esas diferencias funcionales tratadas en el punto anterior para poder manejar este tipo de archivo. Variable de archivo: Para poder explotar las características funcionales de los archivos binarios, esto es poder calcular la posición de un registro y poder leerlo de manera directa o modificar uno o más datos de un archivo sin tener que volverlo a escribir completamente (actualizar datos) es necesario indicar de manera precisa qué tipo de datos vamos a guardar en el archivo. Esto no quiere decir que en un archivo binario un sólo se deba guardar un tipo de dato, sino que si se quiere guardar en un archivo información de diferente tipo, se 260
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
deberá estructurar esta información de modo que no se pierdan las características inherentes a los archivos binarios (acceso directo, actualización de datos, etc.). Por ejemplo si se desea guardar en un archivo binario el código (Integer), nombre (String) y sueldo (Real) de un grupo de personas, si se agrupan esto datos por entidad, es decir colocar primero el código, nombre y sueldo de una misma persona de manera consecutiva, luego se coloca el código, nombre y sueldo de una segunda persona y así sucesivamente, el acceso a los datos de este archivo podrá hacerse de manera directa sin problema alguno, también se les podrá actualizar. Lo que no se podrá es agregar al archivo encabezados, títulos o pies de páginas que difieran del orden de los datos que rompan la homogeneidad del archivo, como sí se puede hacer en los archivos de texto. Por esta razón la variable de archivo debe indicar claramente la información que va a manejar el archivo, y como esto implicará una descripción de un tipo, primero se deberá definir un nuevo tipo de dato con esta descripción y luego se declarará la variable de archivo, como se hace con los arreglos. La sintaxis para declarar una variable de archivo para un archivo binario es la siguiente: type
Nombre del ti o
=
var
Variable de archivo
:
File of
Ti o de dato
Nombre del ti o
;
;
,
Por ejemplo: program variablesDeArchivosBinarios; type // Si el archivo sólo tendrá valores enteros TArchBinInt = File of Integer; // Si el archivo sólo tendrá valores reales
TArchBinReal = File of Real; var archBinNotas: TArchBinInt; archBinFactores: TArchBinReal; … NOTA IMPORTANTE: En el caso en que se quiera guardar cadenas de caracteres en un archivo binario, sólo se podrá emplear cadenas acotadas, así: type Srt60 = String[60]; TArchBinStr = File of Str60; // Si usted coloca File of String no se guardará en el archivo las cadenas esperadas.
En el caso que quiera guardar en el archivo diferentes tipos de datos, estos se deberán hacer a través de un registro, esto es: 261
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
type Srt60 = String[60]; TReg = Record codigo : Integer; nombre : Str60; // Recuerde que no puede usar String solamente sueldo : Real; end; TArchBinReg = File of TReg; Asignación de un archivo: La asignación de un archivo no difiere de la forma en que se asignan los archivos de textos, esto es: var archBinPersonas: TArchBinInt; begin assign (archBinPersonas, ‘empleados.bin’); // Puede emplear cualquier extensión para el nombre del archivo como la que empleamos aquí (.bin), pero debe recordar que hay extensiones estándar que se usan para identificar la procedencia del archivo, como .txt para archivos de texto, .doc ó .docx par archivos de Microsoft Word, .xls ó .xlsx para Microsoft Excel, etc.
Apertura de archivos: Aquí si se presentan algunas diferencia. En primer lugar en archivos binarios no se pueden abrir los archivos empleando el procedimiento append, esta orden es exclusiva de los archivos de texto. En segundo lugar están la forma como se comportan los procedimientos reset y rewrite. Como hemos dicho, en los archivos binarios se puede actualizar los datos almacenados en él, por esta razón un archivo binario no se abre exclusivamente para leer o escribir sino que se abren simplemente y luego de abiertos se pueden leer o escribir en ellos indistintamente. Entonces, la funcionalidad de estos procedimientos será la siguiente: 1° Procedimiento reset Este procedimiento permite abrir un archivo para leer o escribir en él. El indicador del archivo se coloca al inicio del archivo. El archivo debe existir de lo contrario se producirá un error y se interrumpirá la ejecución del programa. 2° Procedimiento rewrite Rewrite permite abrir un archivo para leer o escribir en él. Si el archivo no existe lo crea, de lo contrario borra el contenido del archivo. Por lo tanto si se usa rewrite, la primera operación que se haga en él no podrá ser de lectura, porque el archivo estará vacío, sin embargo una vez grabado un dato se le podrá leer. Cierre de un archivo Aquí no hay diferencia, la orden es close y se emplea de manera similar a la de los archivos de texto, esto es: close (archBin);
262
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Lo que si se debe tomar en cuenta es lo crítico que resulta el cierre de un archivo binario, como al abrir un archivo binario se puede leer y escribir indistintamente, el olvidarse cerrar el archivo puede ocasionar que el archivo quede inservible. Entrada y Salida de datos Se presentan aquí algunas diferencias con respecto a los archivos de texto. A continuación se enumeran esto casos: 1º Como no existen separadores entre los datos, éstos no están estructurados en función de líneas sino en función de “registros”, entiéndase por registro el tipo de dato en que fue definido el archivo, si es File of Integer; un registro será un valor entero, si es File of TReg; un registro será el conjunto de campos de TReg. En este sentido para leer o escribir datos en un archivo binario ya no se podrá emplear los procedimientos readln ni writeln, sólo se empleará read y write. 2º Sólo se puede leer o escribir un “registro” por operación. Esto es las ordenes tendrán la siguiente forma: read (archBin, dato); ó write (archBin, dato); No se podrán leer varios “registros” a la vez, esto es, no se puede hacer: read (archBin, dato1, dato2, dato3); ó write(archBin, dato1 , dato2, dato3); 3º Si el archivo es definido como File of TReg; no se podrá leer o escribir los datos campo por campo, sino el registro en bloque, esto es, se debe hacer: read (archBin, reg); ó write (archBin, reg); y NO: read (archBin, reg.codigo, reg.nombre, reg.sueldo); ó write (archBin, reg.codigo, reg.nombre, reg.sueldo); 4º La función eof se puede emplear para detectar el fin del archivo pero no se podrá emplear eoln porque no existe el separador o cambio de línea en los archivos.
Acceso secuencial a un archivo binario Como hemos dicho, un archivo binario puede manejarse tanto de manera secuencial como de manera directa, en este punto analizaremos el primer caso ya que es la forma natural de acceso a todo tipo de archivo y porque es la más sencilla. El siguiente programa muestra la manera cómo llenar un archivo con valores enteros de manera secuencial. Hay que tener en cuenta que como lo que se guarda en un archivo binario es una copia exacta del dato en memoria (representación binaria), no se puede crear un archivo binario mediante un editor de palabras como se puede hacer con los archivos de texto. Sólo se podrá crear un archivo binario mediante un programa. program CreaArchivoBinarioDeEnteros; type TArcBinInt = File of Integer; var archBin : TArcBinInt; valor: Integer; 263
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
begin assign(archBin, 'datos.bin'); rewrite(archBin); writeln('Ingrese los datos que guardara en el archive, para terminar ingrese cero (0)' ); repeat read(valor); // Leemos el dato desde la consola if valor <> 0 then write(archBin, valor); // Lo guardamos en el archive binario until valor = 0; close(archBin); end. El programa permitirá la lectura, desde el teclado, de los datos que se desean guardar en el archivo binario y los colocará de manera secuencial en el archivo. Al ejecutar el programa se verá algo similar a lo mostrado en la figura siguiente:
Si luego se desea ver los datos que se han guardado en el archivo no se debe emplear un editor de palabras, como se puede hacer con un archivo e textos. La razón es simple, lo que hacen los procesadores de palabras es recuperar byte a byte la información del archivo y luego muestran estos bytes como caracteres ASCII, esto es si se ha guardado el número 37, en el archivo de textos se encuentran los bytes con valores: 0011 0011 y 0011 0111, ó los valores 51 y 55, que corresponden precisamente a los caracteres ‘3’ y ‘7’ respectivamente. Si por el contrario ese mismo número se almacena en un archivo binario, se guardará la secuencia de 4 bytes: 0010 0101, 0000 0000, 0000 0000 y 0000 0000, si lo tratamos de mostrar con un editor de palabras lo que veremos es %□□□, que corresponden a los caracteres ASCII de esos byte. Por lo tanto no vamos a poder entender el contenido del archivo. El archivo que creamos en la aplicación se puede ver en un procesador de texto de una manera similar a la siguiente:
Como se puede apreciar el contenido no se entiende, sin embargo lo que se guardó en el archivo es la información requerida y suficiente para poder recuperar la información y poderla procesar. La única forma de poder ver, entender y procesar el contenido del archivo es mediante un programa. Por eso, a continuación presentaremos un programa que lea la información del archivo y lo muestre en pantalla. program LeeArchivoBinarioDeEnteros; type TArcBinInt = File of Integer; var archBin : TArcBinInt; valor: Integer; 264
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
begin assign(archBin, 'datos.bin'); reset(archBin); writeln('Contenido del archivo:'); while not eof(archBin) do begin read(archBin, valor); // Leemos el dato desde el archive de texto writeln(valor); // Mostramos el dato en la pantalla end; close(archBin); end. Al ejecutar este programa podremos apreciar lo siguiente:
Una segunda aplicación que presentamos consistirá en la creación de un archivo binario en la que coloquemos mayor cantidad de información. En este caso los datos los vamos a sacar de un archivo de textos en donde se encuentre la información de algunos productos, por ejemplo un listado de medicinas. En este archivo de textos tendremos por cada producto su código (valor entero), la descripción (cadena de caracteres) y el precio unitario (valor real), el archivo es similar al que se muestra a continuación: Medicinas.txt 60509 AMPICILINA 125MG SUSP 90 ML 58.65 73972 VITAMINA E 400 MG C90 CAPS 54.4 96031 CETIRIZINA 10 MG TABS C/10 45.9 43633 ERITROMICINA T 250 MG C/20 39.1 79189 CLORFENAMINA 4MG T20 73.1 81695 LECHE DE MAGNESIA 180ML 15.3 …
Como se indicó, el programa leerá la información de este archivo de textos y lo guardará de manera secuencial en otro archivo pero bajo el formato binario. Para poder implementar este programa debemos tomar en cuenta las características de los archivos binarios, de modo que luego se le pueda explotar al máximo. En este sentido, lo primero que debemos considerara es que cada dato que se guarde en el archivo binario debe tener el mismo tamaño en bytes, por eso si nos enfocamos en los 265
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
datos de manera individual veremos que tenemos tres diferentes tipos (entero, cadenas de caracteres y real), por lo que si consideramos los datos individualmente no conseguiremos una uniformidad en los tamaños. Sin embargo si vemos toda la información de un producto como una unidad (entidad), esto es como un registro ( Record) compuesto por tres campos (código, descripción y precio), la variable que maneje el registro si podrá tener un tamaño fijo. Una vez que se haya decidido trabajar con una variable de tipo registro, se debe solucionar el problema de las cadenas de caracteres. Resulta que las versiones modernas de Pascal manejan las cadenas de caracteres de manera dinámica. Esto se refiere a que cuando se asigna una cadena a una variable de tipos String el sistema no reserva un espacio de memoria de 255 bytes a la variable y luego asigna la cadena, si no que le reserva a la variable el espacio de memoria justo para contener la cadena que se quiere asignar, también existe otros problemas que se refieren al manejo de punteros que no se tratará en este capítulo. Pues bien, si esto sucede no se podrá controlar el tamaño de la información que se guarde en el archivo. Sin embargo, afortunadamente tenemos una salida a este problema y es que las cadenas acotadas (p. e.: String[30]) si reservan la memoria de manera anticipada y fija a las variables, de modo que sin importar la cantidad de caracteres que tenga la cadena que se desea guardar, siempre se emplearán la misma cantidad de byte para almacenarlo (para el ejemplo siempre se almacenarán 30 bytes a pesa que se quiera guardar una cadena como ‘Ana Li’), por lo tanto como regla que debemos adoptar en el manejo de archivos binarios es que las cadenas de caracteres que se empleen deberá ser acotadas de manera obligatoria. Los siguientes dos programas contemplan estas cosas, observe la forma cómo se define la estructura y sobretodo la manera cómo se hace con las cadenas de caracteres. Fíjese también que la lectura y escritura de los datos del archivo de texto debe hacerse campo por campo; no se puede leer o escribir el registro todo como una unidad. En el caso de los archivos binarios, la figura se invierte, aquí la información se trabaja como una unidad por lo que no se puede leer o escribir por separado campo por campo. program CreaArchivoBinarioDeRegistros; type // Primero se definen los tipos de datos que manejarán las cadenas de caracteres // que deber ser necesariamente cadenas acotadas
Str50 = String[50];
// Luego se define el tipo que manejará el registro
TRegMedicina = Record codigo: Integer; descripcion: Str50; // No olvidar que las cadenas deben ser acotadas precioUnit: Real; end; // Finalmente definimos la variable de archivo
TArchMedicina = File of TRegMedicina; var archDatos: Text; // Los datos se tomarán de un archivo de textos archMedic: TArchMedicina; regMed: TRegMedicina; 266
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
begin assign(archDatos, 'Medicinas.txt'); // El archivo de datos es de textos reset(archDatos); assign(archMedic, 'Medicinas.bin'); // El archivo final será binario rewrite(archMedic); while not eof(archDatos) do begin // Se deben leer los datos archivo de textos obligatoriamente campo por campo, // NO SE PUEDE LEER O ESCRIBIR EL REGISTRO COMO UNA UNIDAD // En los archivos de texto se puede usar read, readln, write o writeln
readln(archDatos, regMed.codigo); readln(archDatos, regMed.descripcion); readln(archDatos, regMed.precioUnit);
// Se guardan los datos en el archivo binario como una unidad // NO SE PUEDEN LEER O ESCRIBIR LOS DATOS CAMPO POR CAMPO // En los archivos binarios sólo se puede usar read, write
write(archMedic, regMed); end; close(archDatos); close(archMedic); end.
El programa que lea o procese el archivo creado debe definir una estructura de datos idéntica a la que se empleó en su creación. Esta igualdad no se refiere a los nombres de los campos, si no a la cantidad y tipos de dato de los campos. En otras palabras la estructura empleada debe definir la misma cantidad de campos, con los mismos tipos de datos y en el mismo orden. De no seguir estas recomendaciones el programa podrá leer la información del archivo pero no la interpretará correctamente por lo que no se obtendrán los resultados esperados. program LeeArchivoBinarioDeRegistros; type // Se define las cadenas de caracteres como cadenas acotadas Str50 = String[50]; // Se define el tipo que manejará el registro // Observe que los nombres de los campos no coinciden con el tipo de datos que el // archivo, el resto sí
TRegMedicina = Record cod: Integer; desc: Str50; // No olvidar que las cadenas deben ser acotadas precUnit: Real; end; // Finalmente definimos la variable de archivo
TArchMedicina = File of TRegMedicina; var archMedic: TArchMedicina; regMed: TRegMedicina; 267
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
begin assign(archMedic, 'Medicinas.bin'); reset(archMedic); while not eof(archMedic) do begin // Se lee el registro como una unidad
read(ArchMedic, regMed);
// Se muestra el registro en pantalla. Recuerde que la pantalla se comporta // como un archivo de textos, por lo que se debe imprimir campo por campo
writeln(regMed.cod:10, regMed.desc:35, regMed.precUnit:10:2); end; close(archMedic); end. Al ejecutar el programa podremos observar un resultado similar al siguiente:
Acceso directo a un archivo binario Hasta ahora el manejo que hemos hecho de los archivo binarios difiere muy poco de lo que hicimos con los archivos de textos. Por un lado hemos leído uno a uno los datos y los hemos guardado uno a continuación del otro en el archivo binario y por otro, lo que hemos hecho es leer los datos del archivo binario secuencialmente y los hemos mostrado en la pantalla. Esto se hace porque la manera natural de manejar cualquier tipo de archivo es de manera secuencial y por lo tanto no tenemos que hacer algo extraordinario para poder acceder de esa forma a los datos de un archivo cualquiera sea su formato. En este punto, lo que vamos a mostrar es otra forma de acceder a los datos de un archivo, esta forma se denomina “acceso directo”. Esta forma de acceso no es exclusiva de los archivos binarios, sin embargo el que no lo sea implicará realizar operaciones muy complejas que se escapan a la finalidad de este texto, por eso el acceso directo a archivos lo circunscribiremos sólo a archivos binarios. Una de las cosas en la que debemos enfocarnos para realizar este tipo de acceso es en las funciones y procedimientos que permitan realizar esta labor, por eso a continuación las describiremos: 268
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Procedimiento seek Cuando se abre un archivo, el indicador del archivo se coloca al inicio del mismo, este indicador marca la posición a partir de donde se extraerá o grabará la información del o en el archivo. Conforme se extrae o graba en el archivo este indicador se va desplazando hacia delante de modo que cuando se termina la operación, el indicador del archivo queda listo para realizar otra operación a partir del siguiente registro. Esto sucede tanto en archivos de texto como en archivos binarios. El procedimiento seek permite desplazar el indicador del archivo a voluntad en el archivo. Este procedimiento, en el caso de Pascal, sólo se puede emplear con archivos binarios, sin embargo en otros lenguajes de programación, procedimientos similares se pueden aplicar a cualquier tipo de archivo. El procedimiento seek tiene la siguiente sintaxis: seek(varArch, n); Donde varArch es el nombre de la variable de archivo que queremos procesar y n es un valor entero que indica, indirectamente, que registro queremos procesar. Si esto lo haríamos con el archivo de medicinas de la aplicación anterior, podríamos ejecutarla de la siguiente manera: seek(archMedic, 3); Cuando se ejecuta esta orden, lo que va a hacer el sistema es lo siguiente, primero va a observar cómo fue declarada la variable de archivo, en el ejemplo esta orden vería que fue declarada como: var archMedic: TArchMedicina; y luego: TArchMedicina = File of TRegMedicina; Para finalmente: TRegMedicina = Record cod: Integer; desc: Str50; precUnit: Real; end; De este análisis el sistema podrá determinar cuál es el tamaño en bytes que ocupará todo registro en el archivo (si desea saber el tamaño en bytes que ocupa una variable o tipo de dato en un programa se puede usar la función sizeof, por ejemplo: tam := sizeof(TRegMedicina); - tam := sizeof(Integer);). Luego de obtener el tamaño del registro, el procedimiento multiplicará este valor por el valor de n (segundo argumento del procedimiento) y con esto se tendrá la cantidad de que se desplazará el indicador del archivo a partir de inicio, finalmente el procedimiento realiza este desplazamiento. Quedando de este modo el indicador del archivo listo para leer o escribir del o en el archivo un registro que no necesariamente se encuentre consecutivo al registro anterior que se operó. Por lo expuesto, podemos concluir que por medio del procedimiento seek podemos acceder a cualquier registro del archivo sin tener que hacerlo secuencialmente. 269
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
La figura siguiente muestra el cómo se puede realizar este proceso. archivo Registro 1
Registro 2
Registro 3
Registro 4
Registro 5
Registro 6
…
Registro 5
Registro 6
…
Al hacer: reset(Archivo); el indicador del archivo se coloca al inicio: archivo Registro 1
Registro 2
Registro 3
Registro 4
Indicador del archivo Luego si hacemos por ejemplo seek(Archivo, 4), se calcula el valor 4 * sizeof(Registro) y se desplaza el indicador del archivo a esa posición archivo Registro 1 1
Registro 2 2
Registro 3
Registro 4 4
3
sizeof(registro)
Registro 5
Registro 6
…
El indicador del archivo se coloca al inicio del Registro 5
Si luego hacemos por ejemplo seek(Archivo, 2), primero se coloca el indicador del archivo al inicio, luego calcula el valor 2 * sizeof(Registro) y finalmente se desplaza el indicador del archivo a esa posición archivo 1ro se regresa el indicador del archivo a inicia Registro 1 1
Registro 2 2
Luego se desplaza el indicador al valor calculado
Registro 3
Registro 4
Registro 5
Registro 6
…
El indicador del archivo se coloca al inicio del Registro 3
El siguiente programa muestra este proceso, el programa trabajará con el archivo de medicinas, pero en este caso para hacerlo más didáctico se reducirá el número de registros en el archivo. program AccesoDirectoAUnArchivoBinarioDeregistros; type Str50 = String[50]; TRegMedicina = Record cod: Integer; desc: Str50; precUnit: Real; end; TArchMedicina = File of TRegMedicina; var archMedic: TArchMedicina; regMed: TRegMedicina; n: Integer; begin assign(archMedic, 'Medicinas.bin'); reset(archMedic); 270
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
n:=0; // Primero mostramos los datos secuencialmente en el orden en que se aparecen en el archivo
writeln('Datos del archivo (acceso secuencial):'); writeln; writeln(' No Codigo Descripcion P.U.'); writeln; while not eof(archMedic) do begin inc(n); read(ArchMedic, regMed); writeln(n:3,regMed.cod:10, regMed.desc:35, regMed.precUnit:10:2); end; // Luego mostraremos algunos de los registros en orden aleatorio
writeln('Datos del archivo (acceso directo):'); writeln; writeln(' No Codigo Descripcion P.U.'); writeln; n:=5; // Desplazamos el indicador de archivo 5*sizeof(regMed) bytes desde el inicio
seek(archMedic, n); read(ArchMedic, regMed); // Leemos el registro writeln(n+1:3,regMed.cod:10, regMed.desc:35, regMed.precUnit:10:2); // Observe que se imprime "n+1" en lugar de simplemente "n", porque n no coincide con // el número de registro, así si n = 0 se leerá el 1er registro, si n = 1 se leer el segundo y // así sucesivamente
n:=2; // Desplazamos el indicador de archivo 2*sizeof(regMed) bytes desde el inicio
seek(archMedic, n); read(ArchMedic, regMed); // Leemos el registro writeln(n+1:3,regMed.cod:10, regMed.desc:35, regMed.precUnit:10:2); n:=7; // Desplazamos el indicador de archivo 7*sizeof(regMed) bytes desde el inicio
seek(archMedic, n); read(ArchMedic, regMed); // Leemos el registro writeln(n+1:3,regMed.cod:10, regMed.desc:35, regMed.precUnit:10:2); close(archMedic); end. El resultado de este programa lo mostramos a continuación:
271
Estudios Generales Ciencias Curso: Técnicas de Programación Autor: Miguel Guanira Erazo
Funciones filepos y filesize La función filepos nos indica la posición del indicador del archivo en un momento dado, en otras palabras nos dice cuántos registros desde el inicio está desplazado el indicador del archivo luego de haber realizado alguna operación de lectura o escritura en el archivo. La forma cómo se emplea es la siguiente: var archMedic: TArchMedicina; posicion: Integer; begin … reset(archMedic); … // operaciones de lectura o escritura posición := filepos(archMedic); // posición recibe la ubicación del indicador del archivo Si la función se ejecuta inmediatamente después de la apertura (reset o rewrite) la función filepos nos devuelve cero (0), si se realiza luego de la primera operación de entrada o salida devolverá uno(1). La función filesize nos devuelve la cantidad de registros guardados en el archivo. La forma de utilizarla es la siguiente: var archMedic: TArchMedicina; tam: Integer; begin … reset(archMedic); … tam := filesizes(archMedic); El siguiente programa ilustra el uso de estas funciones: program UsoDeFileposYFileSize; type Str50 = String[50]; TRegMedicina = Record cod: Integer; desc: Str50; precUnit: Real; end; TArchMedicina = File of TRegMedicina; var archMedic: TArchMedicina; regMed: TRegMedicina; n, numReg, posIndArch, r: Integer; begin assign(archMedic, 'Medicinas.bin'); reset(archMedic); n:=0; 272