INFORMÁTICA ALGORITMIA Y PROGRAMACIÓN EN C Pablo Carmona del Barco Dpto. Ingeniería de Sistemas Informáticos y Telemáticos Universidad de Extremadura ÍNDICE Tema 1: Introducción a la programación 1.1. Introducción....................................................................................... 1 1.2. Metodología de la programación........................................................2 1.3. Los lenguajes de programación..........................................................6 Tema 2: Tipos de datos y expresiones 2.1 Tipos de datos, constantes y variables..............................................10 2.1 Expresiones...................................................................................... 15 2.2 Funciones internas............................................................................ 18 2.3 Punteros............................................................................................ 19 2.4 Reglas de prioridad........................................................................... 20 Tema 3: Representación gráfica de los algoritmos y su traducción a C 3.1 Métodos de representación algorítmica............................................23 3.2 Operaciones primitivas.....................................................................24 3.3 Estructura de un programa en C.......................................................29 3.4 Estructuras de control.......................................................................30 Tema 4: Estructuras de datos (I): Arrays. Cadenas de caracteres 4.1 Introducción..................................................................................... 43 4.2 Arrays............................................................................................... 44 4.3 Cadenas de caracteres....................................................................... 49 Tema 5: Modularidad 5.1 Introducción a la modularidad..........................................................55 5.2 Definición de funciones....................................................................57 5.3 Invocación de funciones...................................................................59 5.4 Módulos que no devuelven ningún valor..........................................62 5.5 Módulos que devuelven más de un valor.........................................63 5.6 Arrays como parámetros..................................................................65 Tema 6: Estructuras de datos (II): Registros 6.1 Registros........................................................................................... 70 1 INTRODUCCIÓN A LA PROGRAMACIÓN 1.1 INTRODUCCIÓN Una breve definición de informática es la siguiente: «Ciencia del tratamiento racional, mediante máquinas automáticas, de la información». De ella se derivan dos hechos importantes. Por un lado, el tratamiento de la información es llevado a cabo mediante máquinas automáticas: los ordenadores. Por otro lado, la racionalidad en dicho tratamiento introduce el componente humano como elemento indispensable de cualquier proceso informático, ya que será el ser humano quien determine el modo de tratar la información. La informática es una disciplina académica relativamente joven. Baste con señalar que el término Computer Science no fue acuñado hasta los años 60 por el matemático George Forsythe y que el primer Departamento de Informática en una Universidad se formó en el año 1962. Sin embargo, la vertiginosa evolución de las tecnologías de la información ha llevado a la informática a estar presente en la actualidad en gran parte de los campos científico, económico y social. La principal herramienta que emplea la informática para llevar a cabo las tareas relacionadas anteriormente es el ordenador o computador. Su definición nos aporta nuevas claves: «Máquina automática para el tratamiento de la información que ejecuta programas formados por una sucesión de operaciones aritméticas y lógicas” Es decir, para realizar el tratamiento de la información, el ordenador acepta una entrada (los datos), que mediante un programa transforma para proporcionar una salida (el resultado). Es precisamente en la determinación del programa que el ordenador debe ejecutar donde el ser humano interviene de forma decisiva. La Ilustración 1 esquematiza este proceso para el tratamiento programa ENTRADA datos ORDENADOR SALIDA resultado Ilustración 1: Tratamiento automático de la información mediante el ordenador 1 1. INTRODUCCIÓN A LA PROGRAMACIÓN Lenguaje de programación paridad BLOCK escribir ('Un nº:') leer (n) n mod 2 = 0 if then else escribir ('Es par') escribir ('Es impar') program paridad(input,output); var n:integer; begin write('Introduzca un número entero: '); readln(n); if n mod 2 = 0 then writeln('El número es PAR') else writeln('El número es IMPAR') end. ALGORITMO PROGRAMA Ilustración 2: Relación entre algoritmo y programa automático de la información mediante el ordenador. En general, la secuencia de operaciones necesarias para resolver un problema se denomina algoritmo, que puede considerarse como una fórmula o receta para la resolución de un problema. Cuando se pretende que el problema sea resuelto por un ordenador, dicho algoritmo debe traducirse a una sintaxis adecuada: el programa. Un programa está formado por una serie de instrucciones que se corresponden con los distintos pasos del algoritmo que representa. Además, las instrucciones del programa deben ajustarse a las normas que dicte el lenguaje de programación utilizado. Finalmente, el conjunto de actividades que llevan al desarrollo de un programa informático se denomina programación. En la Ilustración 2 se representan todos estos conceptos. 1.2 METODOLOGÍA DE LA PROGRAMACIÓN Un programador es alguien que resuelve problemas. Para llegar a ser un programador eficaz es necesario primero aprender a resolver problemas de un modo riguroso y sistemático, lo que se denomina metodología de la programación. Aunque un problema puede resolverse normalmente de muchas formas distintas y se requiere cierta dosis de práctica e incluso de habilidad para obtener programas eficientes y de resolución clara, existen pautas que guían al programador de forma genérica para alcanzar una buena solución. En esta sección se estudiarán dichas pautas. La tarea de resolución de problemas se divide en tres fases: Análisis del problema: consiste en definir y comprender el problema en toda su extensión, analizando todo aquello que es relevante para su resolución. Diseño del algoritmo: determinar y representar genéricamente el método de resolución del problema (algoritmo). Resolución del problema mediante el ordenador: traducir el algoritmo a un lenguaje de programación y comprobar que se ha hecho correctamente. 2 1. INTRODUCCIÓN A LA PROGRAMACIÓN 1.2.1 ANÁLISIS DEL PROBLEMA La fase de análisis del problema tiene como objetivo conseguir que el programador alcance a comprender el problema en toda su extensión. Esto requiere que el problema esté bien definido, es decir, que se disponga de una descripción del problema suficientemente detallada. A menudo, es tarea del propio programador desarrollar este “enunciado” y completarlo durante el análisis mediante la celebración de entrevistas con personal experto en el área de aplicación del problema (directivos de la empresa, personal especializado, etc.). La correcta definición del problema debe ir acompañada de la identificación y descripción de los datos de entrada y de salida del problema. Es decir, el programador debe determinar de qué datos relevantes para la resolución del problema se dispone (entrada) y qué información debe proporcionarse como resolución del problema (salida). Un dato de entrada puede definirse como un dato del que depende la salida del problema y que, a su vez, no depende de ningún otro (no puede calcularse a partir de otros datos de entrada). Para cada dato de entrada, deberá especificarse lo siguiente: Identificador del dato, que permita referenciarlo durante la fase de análisis. Descripción del dato: ¿en qué consiste el dato? Procedencia: Un dato de entrada puede solicitarse desde un dispositivo de entrada, generarse aleatoriamente o integrarse directamente en el propio programa. La primera alternativa deberá emplearse cuando se trate de información de entrada que varía habitualmente de una resolución a otra del problema y se indicará como procedencia el dispositivo de entrada receptor (teclado, etc.). La segunda alternativa se corresponde con programas donde alguno de los datos está sujeto al azar (por ejemplo, el valor de la tirada de un dado en un juego de azar). En tal caso, se indicará como procedencia aleatorio. La tercera alternativa es más apropiada cuando se trate de datos de entrada con valores que habitualmente no varían, evitando así que el usuario tenga que introducir dichos valores idénticos en cada resolución del problema. En este caso, se indicará como procedencia dato fijo. Valor (sólo para datos fijos): Cuando el dato se integre directamente en el programa (dato fijo), se indicará el valor asociado. Restricciones (sólo para datos procedentes de un dispositivo de entrada o aleatorios): deberán indicarse las restricciones impuestas a los valores que el dato pueda tomar para que se considere válido. El cumplimiento de estas restricciones deberá posteriormente ser exigido por el programa mediante el correspondiente mecanismo de control de entrada (en el caso de datos procedentes de teclado) o durante la generación de los números aleatorios (en el caso de datos aleatorios). Un dato de salida es aquél que proporciona toda o parte de la solución del problema y deberá poder obtenerse a partir de los datos de entrada especificados en el apartado anterior. Para cada dato de salida se proporcionará: Identificador del dato, que permita referenciarlo durante la fase de análisis. Descripción del dato: ¿en qué consiste el dato? Destino: deberá indicarse el dispositivo de salida hacia el que se dirigirá el dato (monitor, impresora, etc.). 3 1. INTRODUCCIÓN A LA PROGRAMACIÓN Una última sección de la fase de análisis estará reservada a la inclusión de aquellos comentarios que el programador considere oportunos para clarificar aún más el problema a resolver. EJEMPLO 1.1. Se desea realizar un programa que determine el precio de una llamada telefónica entre teléfonos fijos. El precio de la llamada lo constituye el coste por establecimiento de llamada (0.10 euros) y un coste por paso (0.03 euros/paso). La duración de un paso depende del tramo horario en el que se efectúe la llamada, según la siguiente tabla: Tramo 1 Tramo 2 Tramo 3 60 seg. 20 seg. 40 seg. El usuario indicará la duración de la llamada y el tramo horario en el que se efectuó. ANÁLISIS: a) Datos de entrada: EST_LLAM=0.10. Coste del establecimiento de llamada (en euros). Dato fijo. PASO=0.03. Coste de un paso (en euros). Dato fijo. DUR1=60. Duración de un paso en el primer tramo horario (en segundos). Dato fijo. DUR2=20. Duración de un paso en el segundo tramo horario (en segundos). Dato fijo. DUR3=40. Duración de un paso en el tercer tramo horario (en segundos). Dato fijo. duracion: Duración de la llamada (valor entero, en segundos). Teclado. (duracion > 0). tramo: Tramo en el que se efectuó la llamada (valor entero). Teclado. (tramo {1,2,3}). b) Datos de salida: precio: Coste de la llamada (en euros). Monitor. c) Comentarios: Las fracciones de pasos se computarán como pasos completos. Emplearemos una variable intermedia para calcular el número de pasos. 1.2.2 DISEÑO DEL ALGORITMO Como ya sabemos, el algoritmo es la representación de los pasos necesarios para resolver un problema. Estos pasos serán después trasladados a instrucciones que el ordenador podrá ejecutar. Aprender a diseñar algoritmos correctamente se considera esencial en la práctica de la programación, más que el conocimiento específico del lenguaje de programación más novedoso. El algoritmo determinará el método de resolución del problema, y si dicho método se ha desarrollado adecuadamente su traducción a un lenguaje de programación u otro es una cuestión menor. Igualmente, el conocimiento de las particularidades internas de un tipo de ordenador u otro tampoco será crucial para el diseño del algoritmo. Esto se debe a que el algoritmo es independiente tanto del lenguaje de programación que vaya a emplearse como del ordenador donde finalmente se ejecute el programa que lo represente: de ahí su gran importancia. Para la descripción de un algoritmo pueden utilizarse representaciones gráficas (diagramas) o textuales (pseudocódigo, lenguaje natural). Además, el diseño de algoritmos requiere conocer de qué herramientas se dispone para describir los pasos de resolución de un problema y cómo debemos utilizarlas. Esto dependerá de la metodología de programación que vayamos a emplear. En este curso nos centraremos en la 4 1. INTRODUCCIÓN A LA PROGRAMACIÓN metodología de la programación denominada programación estructurada y emplearemos diagramas estructurados para la representación de los algoritmos, los cuales serán estudiados en el siguiente capítulo. La fase de diseño se descompone en dos subfases: a) Parte declarativa: en ella se incluirá la definición de constantes y la declaración de variables que se estudiarán en el Capítulo 2, en este orden. b) Representación algorítmica: descripción del algoritmo empleando alguno de los métodos de representación mencionados. EJEMPLO 1.2. La fase de diseño del problema planteado en el Ejemplo 1.1, empleando pseudocódigo para la representación algorítmica, sería la siguiente: a) Parte declarativa: CONSTANTES EST_LLAM=0.10 PASO=0.03 DUR1=60 DUR2=20 DUR3=40 VARIABLES duracion,tramo,numPasos: entero precio: real b) Representación algorítmica: inicio leer desde teclado duracion mientras duracion ≤ 0 hacer escribir mensaje de error leer desde teclado duracion leer desde teclado tramo mientras tramo < 1 o tramo > 3 hacer escribir mensaje de error leer desde teclado tramo si tramo=1 entonces almacenar en numPasos el valor techo(duracion/DUR1) sino (es tramo=1) si tramo=2 entonces almacenar en numPasos el valor techo(duracion/DUR2) sino (es tramo=2) almacenar en numPasos el valor techo(duracion/DUR3) almacenar en precio el valor EST_LLAM+numPasos×PASO escribir precio fin 1.2.3 RESOLUCIÓN DEL PROBLEMA MEDIANTE EL ORDENADOR Tras el diseño del algoritmo, este debe traducirse a las instrucciones de un lenguaje de programación para que pueda ser resuelto por el ordenador. Esta fase se divide en tres subfases: a) Codificación o implementación del algoritmo: es la conversión de los pasos del algoritmo a las instrucciones equivalentes en un lenguaje de programación. El algoritmo escrito en un lenguaje de programación se denomina programa, código fuente o, simplemente, código. b) Verificación del programa: consiste en la comprobación de que la codificación del algoritmo se ha realizado correctamente, empleando adecuadamente de las reglas gramaticales y sintácticas del lenguaje utilizado. 5 1. INTRODUCCIÓN A LA PROGRAMACIÓN c) Validación del programa: consiste en la comprobación de que los resultados proporcionados por el programa se corresponden con los establecidos en el análisis del problema. Veremos un ejemplo de esta última fase en el siguiente capítulo, una vez que se conozcan las instrucciones en lenguaje C. 1.3 LOS LENGUAJES DE PROGRAMACIÓN Para el desarrollo de un programa es necesario conocer al menos un lenguaje de programación. Esta sección se centra en estos, estableciendo una clasificación atendiendo al grado de abstracción del lenguaje y describiendo ciertas herramientas de programación necesarias para que el ordenador pueda realizar la tarea descrita en el lenguaje de programación empleado. 1.3.1 NIVELES DE ABSTRACCIÓN El nivel de abstracción de un lenguaje se refiere a en qué medida la sintaxis y uso de ese lenguaje se encuentra cercano al modo de trabajar de la máquina o, por el contrario, al modo de pensar y hablar del ser humano. La clasificación de los lenguajes atendiendo al nivel de abstracción está relacionada con la evolución de los lenguajes de programación, ya que el grado de abstracción de los lenguajes ha ido aumentando con el paso de los años. A grandes rasgos, podemos distinguir tres grupos: lenguajes máquina, lenguajes de bajo nivel y lenguajes de alto nivel. LENGUAJES MÁQUINA El ordenador representa internamente la información que maneja utilizando código binario. En los primeros tiempos, los programadores tenían que realizar la tediosa tarea de traducir sus algoritmos directamente a las secuencias binarias que representaban las instrucciones y los datos contenidos en el programa. Estas instrucciones, conocidas como instrucciones de código máquina, constituyen el lenguaje máquina de un ordenador. La ventaja de programar en lenguaje máquina es que el código generado puede ser muy eficiente, es decir, emplear poca memoria y ser muy rápido, y, por tanto, utilizar la mínima cantidad de recursos. Sin embargo, la importancia de sus inconvenientes ha acabado con el uso de este tipo de lenguajes en la actualidad. Uno de estos inconvenientes es la dependencia respecto al hardware del ordenador, ya que, el lenguaje máquina de una familia de microprocesadores (por ejemplo, Intel Pentium) no tiene porqué ser compatible con el de otra. Otro inconveniente es la dificultad en la codificación. Por ejemplo, la operación aritmética 4✶3+5 podría representarse esquemáticamente en un hipotético lenguaje máquina como multiplicación 0110 4 0100 3 0011 dirección 0001 suma 5 dirección 0101 0101 0001 donde puede observarse que tanto los códigos de operación como las direcciones de memoria y los valores se representan mediante código binario. Obsérvese que la equivalencia real en instrucciones máquina sería exclusivamente la cadena binaria 0110010000110001010101010001, difícilmente comprensible de forma directa por el programador. 6 1. INTRODUCCIÓN A LA PROGRAMACIÓN LENGUAJES DE BAJO NIVEL (ENSAMBLADORES) Si bien en los primeros tiempos el programador utilizaba lenguaje máquina para describir sus programas, pronto se extendió la costumbre de emplear, durante el diseño de los algoritmos, mnemotécnicos para representar sus pasos (es decir, abreviaturas en inglés de las instrucciones de código máquina a las que equivalen). Esto permitía a los programadores abstraerse de las particularidades de la representación interna de las instrucciones durante el desarrollo del método de resolución del problema y propició el siguiente paso en la evolución de los lenguajes de programación, los lenguajes ensambladores, donde las instrucciones se expresaban utilizando un lenguaje simbólico (no binario) que aumentó la legibilidad de los programas. Sin embargo, estos lenguajes mantienen su dependencia de la máquina, siendo por tanto cada lenguaje ensamblador específico de una familia de microprocesadores. Además, cada instrucción en lenguaje ensamblador equivale a una única instrucción en código máquina. Por ejemplo, la operación aritmética anterior tendría en un hipotético lenguaje ensamblador un aspecto parecido al siguiente: MUL 4,3,A SUM 5,A Debido a lo todavía complejo y poco intuitivo de su programación, actualmente los ensambladores se utilizan en áreas muy reducidas donde las exigencias en velocidad de ejecución o aprovechamiento de recursos son elevadas. LENGUAJES DE ALTO NIVEL Aunque la introducción de los lenguajes ensambladores supuso un importante paso hacia adelante en la evolución de los lenguajes de programación, aún existían inconvenientes importantes que salvar, tales como la dependencia respecto al hardware y la obligación de describir los métodos de resolución como una secuencia de los pequeños pasos que podían realizarse en lenguaje máquina. Se advirtió la necesidad de proporcionar instrucciones de más alto nivel que permitieran describir acciones de mayor envergadura, las cuales equivaldrían luego a un conjunto de instrucciones en código máquina. De esta idea surgieron los lenguajes de alto nivel, más cercanos al modo de expresarse del ser humano y, por tanto, más fáciles de aprender y de utilizar. Igualmente, debido a su fácil comprensión, los programas escritos en un lenguaje de alto nivel son más fáciles de actualizar y corregir. El lenguaje C, que se trata en este curso, es un lenguaje de alto nivel. El ejemplo anterior se expresaría en C con una única instrucción: A = 4✶3+5 fácilmente entendible. Obsérvese que esta única instrucción en un lenguaje de alto nivel equivale a varias instrucciones en el código máquina y el ensamblador del ejemplo anterior. Además de su mayor legibilidad, otra de las características esenciales de los lenguajes de alto nivel es que son transportables. Esto significa que, a diferencia de los lenguajes de más bajo nivel, el mismo programa puede ser válido con pocos o ningún cambio en distintas plataformas, esto es, en ordenadores con distintos microprocesadores. Por tanto, los lenguajes de alto nivel son independientes del hardware. Sus inconvenientes se corresponden con las ventajas de los lenguajes máquina y ensambladores, ya que tanto en ocupación de memoria como en tiempo de ejecución los lenguajes de alto nivel suelen presentar un consumo mayor que aquéllos. 7 1. INTRODUCCIÓN A LA PROGRAMACIÓN 1.3.2 TRADUCTORES DE LENGUAJES El ordenador sólo entiende directamente programas escritos en lenguaje máquina. Cuando la codificación se realiza utilizando un lenguaje simbólico (ensambladores y lenguajes de alto nivel), será necesario traducir dicho programa al código correspondiente en lenguaje máquina. Como puede suponerse, el programador no tiene que hacer esto por sí mismo, sino que existen programas que realizan dicha traducción de forma automática. Al programa escrito en lenguaje simbólico se le llama programa o código fuente y al resultado de la traducción programa o código objeto: Simbólico Máquina TRADUCTOR Programa Programa fuente objeto Distinguimos tres tipos de traductores: ensambladores, compiladores e intérpretes. Los ensambladores traducen programas escritos en ensamblador a código máquina, mientras que los compiladores e intérpretes traducen programas escritos en un lenguaje de alto nivel. Por otra parte, los intérpretes traducen el programa instrucción a instrucción, de forma que hasta que no terminan de ejecutar la última instrucción traducida no proceden a traducir la siguiente, por lo que no generan programa objeto en disco. Por contra, tanto compiladores como ensambladores traducen primero todo el programa fuente, generando un programa objeto en disco, y una vez terminada la traducción, el programa podrá ejecutarse. Las diferencias entre estos tres tipos de traductores están esquematizadas en la Tabla 1. Ahora bien, cuando se desarrolla un programa, este puede incluir referencias a tareas que no son instrucciones del lenguaje de programación, sino que están descritas en las llamadas bibliotecas de funciones que suelen acompañar al traductor del lenguaje para hacerlo más versátil. Dichas tareas son frecuentes y comunes a muchos programas, tales como operaciones gráficas (dibujar un círculo, cambiar el color del texto), matemáticas (logarítmicas, trigonométricas), etc. Por otro lado, un programa puede hacer referencia a otros programas desarrollados por el propio programador que resuelven tareas no incluidas en la biblioteca de funciones (por ejemplo, C no incorpora una función para calcular el factorial de un número). Estos programas se denominan programas fuentes secundarios y requerirán también un proceso de traducción. Por ello, después de traducir el programa principal mediante un ensamblador o un compilador (que solo conoce la equivalencia a lenguaje máquina de las instrucciones propias del lenguaje que traduce), será necesario unir el programa objeto generado con el resto de módulos donde existan tareas a las que se haga referencia desde dicho programa principal (programas fuentes secundarios y bibliotecas de funciones). Para conseguir el programa ejecutable resultado de esta operación de unión se debe utilizar una herramienta denominada montador, enlazador o linkador. Lenguaje fuente L. ensamblador Traductor Ensambladores Método L. de alto nivel Compiladores Todo el programa (programa objeto en disco) Intérpretes Instrucción a instrucción Tabla 1: Diferencias entre los tres tipos de traductores 8 1. INTRODUCCIÓN A LA PROGRAMACIÓN bibliotecas programa fuente principal programas fuentes secundarios ENSAMBLADOR O COMPILADOR ENSAMBLADOR O COMPILADOR programa objeto ENLAZADOR programa ejecutable otros módulos Ilustración 5: Esquema de funcionamiento de compiladores y ensambladores De este modo, cuando estamos empleando un compilador o un ensamblador, el esquema de funcionamiento es el que se muestra en la Ilustración 5. 9 2 TIPOS DE DATOS Y EXPRESIONES En el capítulo anterior se definió la informática como la ciencia que estudia el tratamiento automático de la información. Ahora bien, un ordenador puede manejar dos categorías de información: instrucciones y datos. Mientras las instrucciones describen el proceso a realizar, los datos representan la información a procesar. En este capítulo estudiaremos las distintas alternativas de que dispone el programador para representar los datos (los tipos de datos), así como el modo de operar con ellos (las expresiones). 2.1 TIPOS DE DATOS, CONSTANTES SIMBÓLICAS Y VARIABLES Toda información, ya sea numérica, alfabética, gráfica o de cualquier otro tipo, se representa internamente en el ordenador mediante una secuencia de bits. En los primeros tiempos de la informática, esto suponía que el programador debía estar familiarizado con dicha representación interna, debiendo realizar por sí mismo la conversión de los datos al correspondiente código binario. Sin embargo, con la introducción del concepto de tipo de datos, los lenguajes actuales permiten que el programador pueda abstraerse de la representación interna de los datos y trabajar con ellos de una forma más natural. En la resolución de problemas mediante ordenadores, la estructura seleccionada para representar los datos con los que trabaja un programa es tan importante como el diseño del algoritmo que determina su comportamiento, ya que de dicha estructura va a depender en gran parte la claridad, eficiencia y requisitos de memoria del programa resultante. Existen dos categorías de datos: simples y estructurados. En este capítulo nos centraremos en los tipos de datos simples y los tipos de datos estructurados se estudiarán en los Capítulos 4 y 6. 2.1.1 TIPOS DE DATOS SIMPLES Un dato simple puede definirse como un elemento de información que se trata dentro de un programa como una unidad (por ejemplo, un número o una letra). El lenguaje C dispone fundamentalmente de tres tipos de datos simples: son los tipos entero, real y carácter. 10 2. TIPOS DE DATOS Y EXPRESIONES TIPO ENTERO El tipo entero se representa en C mediante la palabra clave int y consiste en el subconjunto de valores enteros contenido en el intervalo [-2147483648, 2147483647] (equivalente al intervalo [-231, 231-1] que permite representar 232=4.294.967.296 valores distintos y ocupa 4 bytes1). Un literal entero es una secuencia de uno o más dígitos entre el 0 y el 9, debiendo ser el primer dígito distinto de 0 cuando el literal esté formado por más de un dígito. Al literal puede añadírsele un operador unario positivo o negativo (+ o −), aunque se asumirá el signo positivo cuando no se especifique ningún signo. Sin embargo, no admite el uso de separadores de unidades de millar ni el empleo del separador decimal aunque la parte decimal no sea significativa, debiendo por tanto aparecer únicamente los dígitos que constituyen la cifra. Ejemplos de literales válidos de tipo int son: 5 -15 2003 Ejemplos de literales no válidos como tipo int son: -1050.0 3.456 3,200 034 TIPO REAL El tipo real consiste en un subconjunto de los números reales. Un literal real consta de una parte entera y una parte decimal, separadas por un punto decimal. En C, existen dos tipos básicos para representar valores reales: simple precisión (float) y doble precisión (double). El tipo float ocupa 4 bytes y permite representar valores en el rango [1.2×10 -38, 3.4×1038] (positivo y negativo), además del cero. El tipo double ocupa 8 bytes y permite representar valores en el rango [2.2×10-308, 1.8×10308] (también positivo y negativo), además del cero. En aplicaciones científicas, a menudo se emplea una representación especial para manejar números muy grandes o muy pequeños. Por ello, en C existe una representación denominada notación exponencial donde cada literal consta de un número entero o real (mantisa) y de un número entero (exponente), separados por la letra "e" (en minúscula o mayúscula). Por ejemplo, el literal 1.345e2 es equivalente a 1.345×102 o 134.5, y el literal 2.058e-3 es equivalente a 2.058×10-3 o 0.002058. En el primer valor, el punto decimal se desplaza dos lugares hacia la derecha porque el exponente es +2. En el segundo, el punto decimal se desplaza tres lugares hacia la izquierda porque el exponente es -3. Ejemplos de literales reales válidos en C son: 0.45 .23 1234. 1e10 -2e-3 0.2E4 Ejemplos de literales reales no válidos en C son: 1e0.1 12.0a10 1,345.5 1.345,5 2.3-e5 34 TIPO CARÁCTER El tipo carácter se representa en C con la palabra clave char y consiste en el conjunto finito y ordenado de los caracteres representables por el ordenador (en el caso de los PC's, consiste en el conjunto de caracteres incluido en la tabla de códigos ASCII). Un literal de tipo carácter se representa en C mediante un solo carácter encerrado entre comillas simples o apóstrofos. 1 Los intervalos de valores válidos y el número de bytes ocupados por los datos de cada tipo pueden variar según el compilador y plataforma usadas. 11 2. TIPOS DE DATOS Y EXPRESIONES Ejemplos válidos de literales de tipo carácter de C serán: 'a' ',' '>' 'A' '3' Ejemplos de literales de tipo carácter no válidos en C serán: 'ab' 'A q "b" Existen ciertos caracteres especiales que se representan en C mediante las denominadas secuencias de escape. Algunas de las más frecuentes son las siguientes: Carácter Tabulador horizontal Nueva línea (inicio de la siguiente línea) Retorno de carro (inicio de la línea actual) Carácter nulo Comillas Apóstrofo Barra inclinada Secuencia de escape \t \n \r \0 \" \' \\ 2.1.2 CONSTANTES SIMBÓLICAS Y VARIABLES Tanto las constantes simbólicas como las variables permiten almacenar durante la ejecución del programa valores de los tipos de datos que acabamos de ver. Cada variable o constante simbólica lleva asociado un nombre o identificador que la designa. Los nombres elegidos deben ser significativos, es decir, deben hacer referencia al dato que representan. Por ejemplo: edad IVA para almacenar la edad de una persona para almacenar el porcentaje de IVA Es costumbre habitual por parte de los programadores emplear distinta notación para constantes simbólicas y variables, por ejemplo, emplear sólo letras mayúsculas para las constantes simbólicas y una combinación de mayúsculas y minúsculas para las variables. Aunque esta práctica no es obligatoria, facilita la comprensión del código al permitir determinar a simple vista y en cualquier lugar del programa si un identificador hace referencia a una constante simbólica o a una variable. Las reglas sintácticas que deben seguir los identificadores en C son: El primer carácter debe ser una letra o el carácter de subrayado. El resto pueden ser letras, dígitos o el carácter de subrayado. El diagrama sintáctico2 de un identificador será: letra subraya dígito letra subraya 2 Un diagrama sintáctico representa las reglas de construcción sintácticas de un componente de un lenguaje de programación. Para determinar si una construcción determinada es sintácticamente válida basta con seguir su diagrama sintáctico en el sentido de las flechas e ir comprobando que la construcción cumple los requisitos indicados en el diagrama hasta salir del mismo. 12 2. TIPOS DE DATOS Y EXPRESIONES Además, un identificador en C no puede contener espacios, ni acentos, ni la letra ñ, ni puede coincidir con una palabra clave (por ejemplo, no pueden utilizarse int o float como identificadores). Por último, debe tenerse en cuenta que el lenguaje C es sensible a mayúsculas/minúsculas. Así, los identificadores Suma y suma hacen referencia a distintos datos dentro de un mismo programa. Una constante simbólica denomina un valor que no cambia nunca durante la ejecución del programa. La definición de una constante simbólica consiste en la asociación de un identificador con un valor y en C se realiza de la siguiente forma: #define identificador expresión Una constante simbólica no ocupa espacio en memoria, ya que el propio compilador sustituye cada aparición de la constante simbólica en el programa por su valor correspondiente durante el proceso de traducción. En algoritmia, la definición de constantes simbólicas utiliza el siguiente diagrama: CONSTANTES identificador = expresión A continuación se muestran definiciones válidas de constantes simbólicas construidas utilizando los diagramas sintácticos anteriores: En algoritmia: CONSTANTES LETRA='a' NUMERO=-3.141592 PI=-NUMERO En C: #define LETRA 'a' #define NUMERO -3.141592 #define PI -NUMERO Algunas de las ventajas que puede proporcionar la definición de constantes simbólicas frente al uso directo de literales son las siguientes: Facilita la comprensión del programa: la elección de un identificador alusivo al significado del valor que representa puede facilitar la comprensión del programa. Por ejemplo, si en un programa hacemos referencia al porcentaje del IVA, será más comprensible leer el identificador IVA que el literal 21. Facilita la modificación del código del programa: si es necesario modificar un dato considerado como constante en el programa, la definición de una constante simbólica permite hacerlo en un solo paso en lugar de tener que explorar todo el código en busca de literales que hagan referencia a ese dato. Por ejemplo, si el IVA variase, bastaría con modificar el valor asociado a la constante simbólica en la definición para que dicha modificación actuara sobre todas las referencias incluidas en el programa. No obstante, antes de emplear una constante simbólica en lugar de un literal en un programa, debe considerarse si alguna de las ventajas mencionadas va a ser aprovechada, pues en caso contrario sólo se conseguirá aumentar el tamaño del programa y hacerlo menos claro. Por ejemplo, si en un programa quisiéramos contabilizar el número de valores que el usuario introduce por teclado, no sería adecuado definir una constante simbólica para representar al literal 1 que debe tomar el contador como valor inicial, ya que, por un lado, referirse a dicho valor mediante un 13 2. TIPOS DE DATOS Y EXPRESIONES identificador como UNO o PRIMER_VALOR no facilitaría la lectura el programa y, por otro, el primer valor que toma un contador no es susceptible de cambiar en un futuro (siempre comenzaremos a contar desde el valor 1). Una variable denota a un dato cuyo valor puede cambiar durante la ejecución del programa. Cabe aclarar que el concepto de variable en el ámbito de la programación es distinto que en el ámbito de las matemáticas, ya que en programación las variables no representan incógnitas sino datos almacenados en memoria principal. Cada variable, además de un identificador, lleva asociado un tipo que determina su uso, de forma que dicha variable sólo podrá tomar valores de ese tipo. Por ejemplo, una variable de tipo entero sólo podrá almacenar valores enteros. Toda variable debe ser declarada antes de su utilización. Este proceso servirá para reservar espacio en memoria para el valor asociado a dicha variable. En la declaración deberán especificarse el identificador y el tipo de la variable. En algoritmia, se empleará el diagrama sintáctico siguiente VARIABLES identificador de variable tipo : , utilizando la palabra entero para hacer referencia al tipo entero de C, la palabra real para los diferentes tipos de reales y la palabra carácter para el tipo char. En C, el diagrama sintáctico es el siguiente: tipo identificador de variable ; , Declaraciones válidas de variables serían: En algoritmia: En C: VARIABLES radio,diametro:real grupo:carácter edad:entero float radio,diametro; char grupo; int edad; 2.1.3 EL TIPO CADENA DE CARACTERES 3 Un literal de tipo cadena de caracteres es una secuencia de cero o más caracteres incluidos en la tabla de códigos ASCII y encerrados entre comillas dobles. Todo valor de tipo cadena de caracteres finaliza con el carácter especial '\0' (carácter nulo). Este carácter es insertado automáticamente, por lo que el programador no tendrá que preocuparse de incluirlo en los literales de tipo cadena. Así, 3 Aunque el tipo cadena de caracteres es un tipo estructurado y sus características serán estudiadas en profundidad en el Capítulo 4, se introduce aquí para que se pueda trabajar con información de tipo cadena de caracteres desde un principio. 14 2. TIPOS DE DATOS Y EXPRESIONES el literal "Pulsa una tecla para continuar" es una cadena de caracteres con 31 caracteres (las 30 letras más el carácter nulo que indica el fin de cadena). Las variables de tipo cadena de caracteres permiten almacenar cadenas con una longitud máxima que se indica en la declaración entre corchetes junto al identificador. Por ejemplo, para declarar una variable frase de tipo cadena de caracteres con un máximo de 15 caracteres se utiliza la siguiente sintaxis: char frase[15]; que permitirá almacenar frases de hasta 14 caracteres significativos, pues el carácter nulo de fin de cadena ocupará la última posición. 2.2 EXPRESIONES Una expresión matemática tradicional está formada por una combinación de operandos, operadores y paréntesis. Por ejemplo: ∑ x − x n 2 n−1 En los lenguajes de programación también es posible representar expresiones como una combinación de literales, identificadores (de variables o constantes simbólicas), símbolos de operación básicos, paréntesis y nombres de funciones especiales. Todos estos elementos podrán indistintamente estar o no separados por uno o más espacios. Los literales e identificadores actúan como operandos, mientras que los símbolos de operación y las funciones especiales lo hacen como operadores. Cada expresión toma el valor resultante de aplicar los operadores a los operandos. Dependiendo del tipo de este valor resultante, distinguimos dos categorías de expresiones fundamentales: aritméticas y lógicas. 2.2.1 EXPRESIONES ARITMÉTICAS Los operandos serán habitualmente de tipo numérico (real o entero). Los símbolos de operación básicos son: + * / % suma y signo positivo resta y signo negativo multiplicación división módulo (resto) El operador % se debe aplicar siempre a operandos de tipo entero. Cuando la división se aplique a operandos de tipo entero, el resultado será la división entera (por ejemplo, 5/3 dará como resultado 1, que es la parte entera de 1.666). 15 2. TIPOS DE DATOS Y EXPRESIONES El resultado de una expresión aritmética será de tipo numérico. En concreto, Cuando los operandos implicados en la expresión sean del mismo tipo, el resultado será de ese tipo. Por el contrario, cuando en una expresión aritmética se incluyan operandos numéricos de distinto tipo, el resultado obtenido será del tipo correspondiente al operando de mayor precisión. Por ejemplo, si un operando es double y el otro float, el resultado será double, si un operando es int y el otro float, el resultado será float, etc. Para modificar el tipo de un operando o del resultado de una expresión es posible utilizar la conversión explicita de tipos (casting). Para ello, basta con preceder a la expresión que quiere convertirse a un nuevo tipo, de dicho tipo encerrado entre paréntesis. Así, aunque f sea una variable de tipo float, (int) f % 2 será una expresión válida, pues el operando f ha sido convertido previamente al tipo int antes de aplicar la operación de módulo (adviértase que en esa conversión se descartaría la parte decimal del valor de f). Reglas de prioridad: Es posible que en una expresión aritmética concurran varios operadores. En ese caso, el orden de aplicación de los mismos puede influir en el resultado final. Por ejemplo, en la expresión aritmética 6+4/2 el resultado será 5 u 8 dependiendo del orden en que se apliquen los operadores + y /. Por lo tanto, es necesario establecer unas reglas para determinar qué operadores se aplican primero en estos casos. Estas reglas se denominan reglas de prioridad y en C son las siguientes: 1. Operadores unarios signo positivo (+), signo negativo (-) y casting (tipo) 2. Operadores *, /, %. 3. Operadores binarios suma (+) y resta (-). Si coincidieran varios operadores de igual prioridad en una expresión, el orden de evaluación es de izquierda a derecha, excepto para los operadores unarios que será de derecha a izquierda. Por ejemplo, el resultado de 4+8/-2*6 es -20. Los paréntesis permiten alterar la prioridad por defecto de los operadores, ya que las expresiones encerradas entre paréntesis serán las primeras que se evalúen. Si existen paréntesis anidados (interiores unos a otros), las operaciones internas se evalúan primero. Por ejemplo, el resultado de (4+8)/(-2*6) es -1. 2.2.2 EXPRESIONES LÓGICAS Las expresiones lógicas permiten incluir en un programa instrucciones que se ejecutarán solamente bajo ciertas condiciones. Podrán tomar únicamente dos posibles valores: verdadero (1) o falso (0). Se forman combinando identificadores (de variables o constantes simbólicas), literales y subexpresiones con operadores relacionales (de relación o comparación) y operadores lógicos. 16 2. TIPOS DE DATOS Y EXPRESIONES Los operadores relacionales permiten realizar comparaciones. Son los siguientes: < > == <= >= != menor que mayor que igual a menor o igual a mayor o igual a distinto de El formato general de una expresión relacional es: expresión1 operador de relación expresión2 La aplicación a valores numéricos es evidente. Si X=4 e Y=3, entonces: X > Y es 1 (verdadero) (X - 2) < (Y – 4) es 0 (falso) Cuando se comparan valores de tipo carácter, el resultado de la comparación será el de la comparación de los valores ASCII asociados a los caracteres implicados. Por ejemplo, 'A'<='B' dará como resultado 1 (verdadero), ya que el código ASCII de la A (65) es menor que el de la B (66), mientras que 'X'=='Z' dará como resultado 0 (falso), ya que el código ASCII de la X (88) es distinto del de la Z (90). Los operadores relacionales no pueden aplicarse directamente a valores de tipo cadena de caracteres. Las reglas de prioridad entre operadores relacionales son: 1. <, <=, >, >= 2. ==, != Los operadores lógicos son: ! (no-lógico), && (y-lógico) y || (o-lógico). Se aplican casi siempre a operandos que son subexpresiones lógicas, aunque en general pueden aplicarse a cualquier valor entero. En este sentido, el 0 se interpretará como falso y cualquier entero distinto de 0 (no solo el 1) se interpretará como verdadero. El resultado será 1 o 0 (verdadero o falso, respectivamente). Los operadores && y || son operadores binarios, mientras que ! es unario. Los operadores &&, || y ! vienen definidos por sus tablas de verdad: a b a && b distinto de 0 distinto de 0 0 0 distinto de 0 0 distinto de 0 0 1 0 0 0 a b a || b distinto de 0 distinto de 0 0 0 distinto de 0 0 distinto de 0 0 1 1 1 0 Devuelve un resultado verdadero si y sólo si los dos operandos son verdadero Devuelve un resultado verdadero si uno cualquiera de los operandos es verdadero 17 2. TIPOS DE DATOS Y EXPRESIONES a !a distinto de 0 0 0 1 Devuelve un resultado opuesto al del operando Las reglas de prioridad entre operadores lógicos son: 1. no-lógico (!) 2. y-lógico (&&) 3. o-lógico (||) Si aparecen dos o más operadores lógicos iguales en una expresión, los operadores ! se aplicarán de derecha a izquierda y los operadores && y || de izquierda a derecha. Por ejemplo, si X=4 e Y=3, la expresión lógica (X > 3) || (Y > 5) && !(X=4) será verdadero (1). 2.3 FUNCIONES ESPECIALES Los operadores básicos que acaban de estudiarse no son suficientes para realizar ciertas operaciones de cálculo (trigonométricas, logarítmicas, etc.). No obstante, algunas de estas operaciones se encuentran disponibles en los lenguajes de programación a través de bibliotecas de funciones y se denominan funciones internas. Otras, como veremos en el Capítulo 5 pueden ser definidas por el programador y se denominan funciones externas. Cada función, cuando es invocada desde un programa, provoca la ejecución de un conjunto específico de instrucciones y puede devolver uno o más valores, que en cada caso serán de un tipo determinado. La mayoría de las funciones requieren datos de entrada que se denominan parámetros y que aparecen entre paréntesis detrás del nombre de la función. Estos parámetros también deberán ser datos de un tipo concreto, que vendrá determinado por la función en cuestión. Además, para poder utilizar una determinada función interna, es necesario conocer en qué biblioteca se encuentra e incluir al comienzo del programa una instrucción para hacer referencia al archivo de cabecera de la biblioteca correspondiente, que será un archivo con extensión “.h” (por ejemplo, stdio.h para las funciones de entrada/salida estándar o math.h para las funciones matemáticas). El diagrama sintáctico de esta instrucción es el siguiente: #include < archivo de cabecera > Los tipos de datos de los parámetros y del valor devuelto por algunas de las funciones internas más usuales se muestran en la siguiente tabla. El tipo de los parámetros se representa entre los paréntesis de las funciones (i=int, d=double, c=char), si bien los parámetros de tipo double también admiten valores de cualquier otro tipo numérico (que será convertido a double). 18 2. TIPOS DE DATOS Y EXPRESIONES Función Tipo Devuelto Cabecera Propósito abs(i) int math.h Valor absoluto de i fabs(d) double math.h Valor absoluto de d ceil(d) double math.h Redondeo por exceso (el entero más pequeño mayor o igual a d) floor(d) double math.h Redondeo por defecto (el entero más grande menor o igual a d) cos(d) double math.h Coseno de d sin(d) double math.h Seno de d tan(d) double math.h Tangente de d exp(d) double math.h Exponencial de d (ed) log(d) double math.h Logarítmo neperiano de d pow(d1,d2) double math.h Potencia de d1 elevado a d2 sqrt(d) double math.h Raíz cuadrada de d tolower(c) char ctype.h Convertir c a minúsculas toupper(c) char ctype.h Convertir c a mayúsculas 2.4 PUNTEROS Un puntero es un dato que representa la dirección de memoria donde se almacena otro dato. Por ello, se dice que el valor de un puntero apunta a otro valor. Los punteros son usados muy frecuentemente en C y están relacionados con otros conceptos como los de arrays y modularidad que se estudiarán en capítulos posteriores. Para trabajar con punteros es necesario conocer los operadores de dirección e indirección. El operador de dirección (&) permite obtener la dirección de memoria en la que se encuentra almacenado un dato (es decir, la dirección de memoria asociada a una variable). El resultado de la operación de dirección será, por tanto, de tipo puntero. Por ejemplo, si v es una variable declarada de tipo real, en el siguiente esquema &v 65522 v 3.154 v representa el contenido de la variable (el valor real 3.154) y &v representa la dirección de memoria donde se almacena dicho valor (la posición 65522). Se dice que &v es un puntero a v. El operador de indirección (*) permite obtener el valor al que apunta un puntero (es decir, el valor contenido en la dirección de memoria que representa el puntero). Por ejemplo, si pv es una variable puntero que apunta a la variable v (es decir, pv es igual a &v), entonces *pv representa el valor al que apunta la variable puntero pv (es decir, *pv es igual a v). En el siguiente esquema se representa la relación entre v y pv mediante los operadores de dirección e indirección: pv 65520 pv=&v 65522 65522 *pv=v 3.154 19 2. TIPOS DE DATOS Y EXPRESIONES La declaración de una variable de tipo puntero requiere indicar el tipo del valor al que apuntará el puntero y preceder al identificador del operador de indirección. En el ejemplo anterior, la declaración de pv tanto en algoritmia como en C sería: En algoritmia: VARIABLES *pv:real En C: float *pv; 2.5 REGLAS DE PRIORIDAD A continuación se muestran las reglas de prioridad y el orden de evaluación de todos los operadores vistos en este capítulo: 1. Funciones especiales 2. + (positivo), - (negativo), !, (tipo) (casting), &, * (indirección) 3. * (multiplicación), /, % 4. + (suma), - (resta) 5. <, >, <=, >= 6. ==, != 7. && 8. || Los operadores de la misma prioridad se aplicarán de izquierda a derecha, exceptos los operadores unarios (!, +, -, (tipo), & y *) que se aplicarán de derecha a izquierda. 20 2. TIPOS DE DATOS Y EXPRESIONES EJERCICIOS 1º) Indicar cuáles de los siguientes identificadores no son válidos: a) _Id d) Id-1 g) xXx j) años b) 23 e) D/2 h) D3 c) duración f) 3D i) μm 2º) Indicar cuáles de los siguientes literales son válidos y, de los válidos, de qué tipo son: a) 432 d) -9.237 g) -87E-5 j) "A" b) 40,000 e) 5/3 h) 3.5e+1 k) '\"' c) 2.7×10 f) 2,933 i) '9' l) 24e1.2 3º) Escribir las siguientes expresiones algebraicas como expresiones aritméticas en C, empleando el menor número posible de paréntesis: e) 3 2 a b a b i) ln 2 y 2 − y−10 c) y2 f) 25×10−4 x 2y j) a c2 2 a b 2b × d) 2 c b2 c 2a g) q3 q−2 k) 2 h) b 2 −4a c a) 8 x−3 x y 2 b) 20 ' 2 x 24 ' 5 x−40 l) x 1 x 2 y 3,2 y 2 l g 2cos 1−tan 2 4º) Escribir las siguientes expresiones aritméticas en C como expresiones algebraicas: a) sqrt(x/y+x/z) b) x+3/y+2 c) exp(-pow(1-2*x/a,2)) 5º) Determinar el valor y tipo del resultado de las siguientes expresiones o decir si no son válidas: a) b) c) d) e) f) g) h) i) j) k) l) sqrt(25)/2 pow(sqrt(9),2) 44%(6/3.0) 44%6/3.0 16/5%3 16/5*5 16/5*(double) 5 16/(double)5*5 !(floor(6.6)==ceil(5.3)) || *&p==p && ceil(5./3)==5/3 4/2*3/6+fabs(6/2-pow(3,2))*2 "A" <= "B" 6>2 && 5<4 || !(toupper(tolower('A'))=='A') 21 2. TIPOS DE DATOS Y EXPRESIONES 6º) Suponiendo la siguiente información en memoria, responder a las siguientes preguntas: a) b) c) d) ¿Cuál sería el valor de *v2, si v2 vale &v1? ¿Cuál sería el valor de *v2, si v2 vale v1? ¿Cuál sería el valor de &v2, si v2 vale &v1? ¿Cuál sería el valor de &v2, si v2 vale v1? 7º) Suponiendo la siguiente información en memoria, responder a las siguientes preguntas: a) b) c) d) e) f) ¿Cuál sería el valor de *v3, si v3 vale &v1? ¿Cuál sería el valor de *v3, si v3 vale v1? ¿Cuál sería el valor de *v3, si v3 vale &v2? ¿Cuál sería el valor de *v3, si v3 vale v2? ¿Cuál sería el valor de &v3, si v3 vale &v1? ¿Cuál sería el valor de &v3, si v3 vale &v2? 22 3 REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C En el Capítulo 1 estudiamos que un algoritmo nos permite describir paso a paso el proceso necesario para resolver un problema. La representación de un algoritmo se hace bien mediante texto (pseudocódigo), bien mediante fórmulas, o bien de forma gráfica utilizando símbolos geométricos en lo que se denominan diagramas. La representación de algoritmos mediante diagramas es una forma clara e intuitiva de describir los distintos pasos del método de resolución, su orden y su estructura. En este capítulo se tratará la representación de los distintos elementos de un algoritmo mediante diagramas. Paralelamente, se irán introduciendo las reglas sintácticas para traducir a lenguaje C cada uno de esos elementos. 3.1 MÉTODOS DE REPRESENTACIÓN ALGORÍTMICA Las técnicas de programación tienen un papel primordial en el desarrollo del software. Una de estas técnicas es la llamada programación estructurada, desarrollada por Edsger W. Dijkstra1 en 1972 y referida a un conjunto de directrices que aumentan considerablemente la productividad de los programadores, reduciendo el tiempo requerido para escribir, depurar y mantener los programas. La programación estructurada se basa en el Teorema de la Estructura, enunciado por Bohm y Jacopini2 en 1966 y según el cual todo programa puede ser escrito utilizando solamente tres tipos de estructuras básicas (también llamadas estructuras de control): secuenciales, selectivas y repetitivas. Este teorema permite minimizar la complejidad de los métodos de resolución al reducir el juego de estructuras empleado por los programas y facilitar así el seguimiento de la lógica de los mismos. 1 Edsger W. Dijkstra. Notes on structured programming, en Ole-Johan Dahl, Edsger W. Dijkstra y C. A. R. Hoare, editores, Structured Programming. Academic Press, 1972. 2 Bohm, C. y Jacopini, G, Flow Diagrams, Turing Machines and Languages with Only Two Formation Rules, Communications of the ACM, No. 5, Mayo 1966, págs. 366-371. 23 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C Nombre del programa BLOCK 1 2 3 3.1 3.2 3.1.1 3.1.2 4 3.3 3.3.1 3.3.2 Ilustración 1: Un ejemplo de diagrama de Tabourier Los diagramas estructurados son herramientas que permiten representar adecuadamente algoritmos que respetan las reglas de la programación estructurada. Aunque existen diferentes métodos, en este libro nos centraremos en el denominado método de Tabourier (Ilustración 1), según el cual, todo diagrama estructurado comienza por un rectángulo dividido horizontalmente, en cuya parte superior aparece el nombre del programa y en la inferior la palabra BLOCK. De este rectángulo parten, unidos mediante líneas, los pasos que conforman el algoritmo encerrados en rectángulos y rombos. La estructura arborescente resultante se recorre en preorden, es decir, de izquierda a derecha y de arriba a abajo, de forma que la operación situada a la derecha de la actual se examina sólo después de haber examinado todas las que “cuelgan” de dicha operación actual. 3.2 OPERACIONES PRIMITIVAS Las operaciones primitivas son los elementos básicos de cualquier algoritmo o programa. Éstas, junto con las estructuras de control que se estudiarán a continuación, nos permitirán construir cualquier programa. En C, las instrucciones primitivas acaban todas en punto y coma (;), las instrucciones de control, no. 3.2.1 LA OPERACIÓN DE ASIGNACIÓN La operación de asignación permite almacenar valores en las variables. El operador de asignación se representa en un algoritmo mediante el símbolo ←. El formato general de una operación de asignación en un diagrama de Tabourier es: identificador de variable ← expresión Por ejemplo, la operación edad ← 25 asigna a la variable de identificador edad el valor 25. Hay que tener en cuenta que el valor que la variable pudiera contener antes de la asignación es sustituido por el nuevo. 24 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C El operador de asignación tiene menor prioridad que cualquier otro. Esto hace que en primer lugar se calcule el valor de la expresión al lado derecho del operador, y en segundo lugar este valor se almacene en la variable cuyo nombre aparece a la izquierda del operador de asignación. Es posible utilizar el mismo identificador en ambos lados del operador de asignación, dando lugar a operaciones de conteo o de acumulación. Por ejemplo, total ← total + 1 será una operación de conteo que incrementa en 1 el valor de la variable total, mientras que total ← total + incremento será una operación de acumulación que incrementa el valor de la variable total en una cantidad igual al valor almacenado en la variable incremento. Una instrucción de asignación en C utiliza como operador de asignación el carácter igual (=) y finaliza en punto y coma (;). Por ejemplo, edad=25; correspondería a la operación de asignación anterior. Adviértase la diferencia entre este operador y el operador relacional “igual que” (==) estudiado en el capítulo anterior. El primero asigna un valor a una variable, mientras que el segundo compara dos valores para determinar si son iguales. Es habitual entre los programadores noveles confundir ambos operadores. En C, el tipo del valor obtenido al evaluar el lado derecho de una instrucción de asignación se convertirá al tipo de la variable que se encuentra en el lado izquierdo, lo que puede provocar una alteración del valor realmente almacenado (por ejemplo, al asignar el valor real 5.45 a una variable de tipo entero, se almacenará el valor 5). Por ello, para evitar errores, es recomendable en general que el dato que se asigna pueda almacenarse sin pérdidas en la variable especificada (por ejemplo, el valor entero 5 podrá almacenarse sin pérdidas en una variable de tipo entero o de tipo real). 3.2.2 OPERACIONES DE ENTRADA Las operaciones de entrada permiten leer valores desde un dispositivo de entrada (por ejemplo, el teclado) y asignarlos a variables. Esta operación también se denomina operación de lectura, indicando que el programa (el ordenador) va a leer cierta información del exterior (usuario). En un diagrama de Tabourier, una operación de entrada desde teclado se representa: leer (id1, id2, ..., idN) En C, las operaciones de entrada se realizan fundamentalmente a través de la función scanf que utilizaremos para asignar a variables valores de tipo simple (carácter, entero o real) introducidos por teclado. Su archivo de cabecera es stdio.h, que debe incluirse mediante la instrucción #include estudiada en capítulo anterior. La función scanf emplea el siguiente diagrama sintáctico: scanf( cadena de control , & id. variable tipo simple ); 25 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C La cadena de control debe contener un número de especificaciones de conversión igual al número de identificadores de variables que le sucedan. Cuando hay más de una variable, las especificaciones de conversión deben aparecer separadas entre sí por espacios y quedan asociadas por orden a las correspondientes variables (la primera especificación de conversión con la primera variable, la segunda con la segunda, etc.). Cada especificación de conversión es un grupo de caracteres que indica el tipo de la variable correspondiente según la siguiente tabla: Especificación de conversión Tipo %d int %f float %lf double %c char Obsérvese que cada identificador de variable viene precedido por el operador de dirección (&), es decir, tras la cadena de control se indican las direcciones de las variables donde se almacenarán los valores introducidos desde teclado. Cuando se utiliza una misma instrucción para leer más de un valor numérico, éstos deben introducirse desde teclado separados por una pulsación de la tecla Intro o espacios. Para finalizar la entrada se pulsará la tecla Intro. EJEMPLO 3.1. Dadas las variables grupo, habitantes y masa, declaradas como char, int y double, respectivamente, podrán recibir valores desde el teclado utilizando la siguiente instrucción de entrada: scanf("%c %d %lf", &grupo, &habitantes, &masa); Los valores deberán introducirse desde teclado separados entre sí mediante la tecla Intro o espacios y la introducción deberá finalizar con la tecla Intro, por ejemplo: B 146832 1.9891e30 o bien B 146832 1.9891e30 Obsérvese que en la instrucción scanf las especificaciones de conversión van separadas entre sí por espacios y que cada identificador va precedido del carácter ampersand (&). Por otro lado, los valores reales podrá introducirlos el usuario indistintamente en notación clásica o en notación exponencial. Para la introducción desde teclado de cadenas de caracteres, emplearemos una función específica para este propósito: la función gets. Para utilizarla se incluirá entre paréntesis el identificador de la variable de tipo cadena a la que se asignará la cadena de caracteres introducida desde teclado. Para evitar ciertos problemas cuando se emplea en combinación con la función scanf, cada llamada a gets la precederemos de una llamada a la función fflush(stdin);, que borra el buffer de teclado. EJEMPLO 3.2. Dada la variable pelicula declarada como char[60], podrá recibir una cadena desde teclado utilizando la siguiente instrucción de entrada: fflush(stdin); gets(pelicula); 26 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C La cadena deberá introducirse seguida de la tecla Intro, por ejemplo: La Guerra de las Galaxias Obsérvese que en la función gets el identificador ahora no va precedido del carácter ampersand (&). 3.2.3 OPERACIONES DE SALIDA Las operaciones de salida (también llamadas de escritura) permiten visualizar el valor de variables o expresiones a través de un dispositivo de salida (por ejemplo, el monitor). En un diagrama de Tabourier, las operaciones de escritura sobre pantalla se representan: escribir (exp1, exp2, ..., expN) En C, utilizaremos la función printf para mostrar por pantalla valores de cualquier tipo (enteros, reales, caracteres y cadenas), cuyo archivo de cabecera es también stdio.h. Su diagrama sintáctico es el siguiente: printf( cadena de control ); expresión , En la instrucción printf, la cadena de control deberá contener una especificación de conversión por cada expresión y además podrá contener otros caracteres cualesquiera (incluidas las secuencias de escape vistas en el capítulo anterior) que se mostrarán por pantalla. El valor de las expresiones se insertará en la cadena de control allí donde aparezca su correspondiente especificación de conversión. Las especificaciones de conversión más habituales con la instrucción printf y sus formatos de salida asociados se muestran en la siguiente tabla: Especificación de conversión Tipo %d int %f float, double (notación clásica) %e float, double (notación exponencial) %g %c float, double (notación clasica/exponencial dependiendo de la precisión) char %s char [] 27 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C EJEMPLO 3.3. Dadas las variables grupo, habitantes y masa declaradas como char, int y double, respectivamente, y con los valores del Ejemplo 3.1, la siguiente instrucción de salida: printf("Grupo=%c\nBadajoz=%d hab.\nMasa Sol=%e kg.\n",grupo,habitantes,masa); generará las siguientes tres líneas en pantalla: Grupo=B Badajoz=146832 hab. Masa Sol=1.989100e+30 kg. Obsérvese que el valor de las variables se ha insertado en el lugar de las correspondientes especificaciones de conversión dentro de la cadena de control. En estas instrucciones los datos se visualizan con un determinado formato por defecto que depende del tipo del dato. Por ejemplo, en el caso de los reales, las instrucciones a=15.2; printf("%f",a); producirían el resultado 15.200000 mostrando por tanto el valor de la variable a con 6 dígitos decimales, aunque 5 de ellos no sean significativos. Para que la información pueda mostrarse de forma más clara, es posible añadir ciertos modificadores a las especificaciones de conversión. Uno de estos modificadores permite indicar el número mínimo de caracteres que ocupará en pantalla el valor correspondiente. Cuando el valor a mostrar tenga menos caracteres que el número de caracteres reservado, el valor será precedido de espacios hasta completar dicho número. Cuando el valor tenga más caracteres que los reservados, el valor no aparecerá truncado, sino que se tomarán los caracteres necesarios por la derecha. Este modificador se indica mediante un número situado inmediatamente después del carácter % en la especificación de conversión correspondiente. EJEMPLO 3.4. Dadas las variables grupo, habitantes y masa del ejemplo anterior, la siguiente instrucción de salida: printf("Grupo=%4c\nBadajoz=%5d hab.\nMasa Sol=%13e kg.\n",grupo,habitantes,masa); generará las siguientes tres líneas en pantalla: Grupo= B Badajoz=146832 hab. Masa Sol= 1.989100e+30 kg. Obsérvese que el valor de grupo y de masa aparece precedido de 3 y 1 espacios para completar los 4 y 13 caracteres reservados, respectivamente. Sin embargo, aunque para habitantes se han reservado 5 caracteres y el valor ocupa 6, se visualiza el dato completo. En el caso de los valores reales, es posible indicar un modificador adicional que especifique el número de dígitos decimales que se mostrarán por pantalla. El valor a mostrar se redondeará, si es preciso, al número de decimales indicado por el modificador. Este modificador se incluirá en la especificación de conversión mediante un punto seguido de un número entero, tras el carácter % (es decir, %.entero) o, si se especifica el número mínimo de caracteres reservados, tras dicho número (es decir, %entero.entero). 28 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C EJEMPLO 3.5. Dada la variable masa del ejemplo anterior y la variable numeroPi de tipo float y valor 3.14159265, la siguiente instrucción de salida: printf("Masa Sol=%.2e kg.\nNúmero PI=%8.4f\n",masa,numeroPi); mostrará en pantalla las siguientes líneas: Masa Sol=1.99e+30 kg. Número PI= 3.1416 3.3 ESTRUCTURA DE UN PROGRAMA EN C El esqueleto de un programa en C simple tiene el siguiente aspecto: inclusión de archivos de cabecera (#include) definición de constantes (#define) int main() { declaración de variables instrucciones del programa return 0; } En este esqueleto encontramos los siguiente elementos: • En primer lugar se sitúan las instrucciones #include correspondientes a los archivos de cabecera de las funciones internas que se vayan a utilizar en el programa, seguido de la definición de las constantes simbólicas (si hubiera alguna), según se estudió en el Capítulo 2. • La siguiente línea int main() es la cabecera de la función principal, por donde todo programa comienza a ejecutarse, y el contenido de dicha función se encuentra a continuación entre llaves. El significado de su sintaxis se explicará en el Capítulo 5. • La declaración de variables deberá incluir todas las variables utilizadas en el programa, según la sintaxis estudiada en el Capítulo 2. • Finalmente, se incluirán las instrucciones ejecutables correspondientes a la traducción de los pasos del algoritmo representados en el diagrama de Tabourier, seguidas por la instrucción return 0 para finalizar la ejecución del programa. EJEMPLO 3.6. El siguiente programa en C permite calcular y mostrar por pantalla la suma de dos números enteros leídos desde teclado. #include <stdio.h> int main() { int s1,s2,result; } printf("Introduzca dos números enteros: "); scanf("%d %d",&s1,&s2); result=s1+s2; printf("La suma de %d más %d es %d.\n",s1,s2,result); return 0; 29 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C 3.4 ESTRUCTURAS DE CONTROL A continuación, se estudiará el significado y la representación de las estructuras básicas de la programación estructurada (secuenciales, selectivas y repetitivas), así como las distintas variantes que existen. 3.4.1 ESTRUCTURA SECUENCIAL Es aquélla en la que todas las acciones que la componen se ejecutan exactamente una vez. La figura siguiente representa una estructura secuencial: BLOCK 1 3 2 ... n EJEMPLO 3.7. Realizar un programa que calcule el perímetro y el área de un rectángulo a partir de la base y la altura dadas por el usuario. ANÁLISIS: a) Datos de entrada: bas: base del rectángulo. Teclado. (bas > 0)3 alt: altura del rectángulo. Teclado. (alt > 0)3 b) Datos de salida: per: perímetro del rectángulo. Monitor. area: área del rectángulo. Monitor. DISEÑO: a) Parte declarativa: VARIABLES bas,alt,per,area:real b) Representación algorítmica: rectangulo BLOCK escribir leer per←2bas+2alt area←basalt escribir ("Base y altura:") (bas,alt) (per,area) 3 Dado que aún no se han estudiado las estructuras que permitirán realizar el control de entrada correspondiente a estas restricciones, dicho control se omitirá excepcionalmente en la representación algorítmica de este ejemplo. 30 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C CODIFICACIÓN: /************************************************/ /* Muestra el perímetro y área de un rectángulo */ /************************************************/ #include <stdio.h> int main() { float bas,alt,per,area; } printf("Introduzca la base y la altura del rectángulo: "); scanf("%f %f",&bas,&alt); per=2*bas+2*alt; /* Perímetro del rectángulo */ area=bas*alt; /* Area del rectángulo */ printf("Su perímetro es %.2f y su área %.2f\n",per,area); return 0; 3.4.2 ESTRUCTURAS SELECTIVAS La estructura secuencial es típica de los algoritmos que pueden llevarse a cabo con una calculadora básica, ya que todas las instrucciones introducidas se ejecutan exactamente una vez. El empleo de ordenadores para la ejecución de algoritmos cobra mayor sentido cuando en ellos se describe algo más que una mera secuencia de acciones. Este es el caso cuando el siguiente paso a ejecutar depende del valor de una expresión. En las estructuras selectivas se evalúa una expresión y en función de su resultado se determina cuál será el siguiente paso. La acción asociada a cada una de las alternativas consideradas en la estructura selectiva se denomina cuerpo de dicha alternativa. ESTRUCTURA SELECTIVA SIMPLE (IF-THEN) Esta estructura restringe la ejecución de una acción al cumplimiento de una condición. Su representación en un diagrama de Tabourier es la siguiente: if then expresión lógica acción En la ejecución de una estructura selectiva simple, en primer lugar se evalúa la condición. Si el resultado de la expresión lógica es verdadero se ejecuta la acción indicada; en caso contrario, no se ejecuta. EJEMPLO 3.8. La siguiente porción de algoritmo expresa que la operación de asignación se ejecutará solo si la variable x es mayor que 0. if then x>0 positivo ← 1 31 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C Es posible que la acción a realizar si se cumple la condición sea una acción compuesta, esto es, que conste de varios pasos más simples. Una acción compuesta en un diagrama de Tabourier se representará mediante una estructura secuencial: BLOCK paso 1 paso 2 paso n ... Por tanto, una estructura selectiva simple cuando contiene una acción compuesta se representará en un diagrama de Tabourier de la siguiente forma: if then expresión lógica BLOCK paso 1 paso 2 paso n ... En C, cuando el cuerpo de una estructura selectiva simple consiste en una acción sencilla (es decir, una sola instrucción), se utiliza el siguiente diagrama sintáctico: expresión ) lógica Las acciones compuestas en C emplean el diagrama sintáctico: if ( { instrucción } instrucción Por tanto, la estructura selectiva simple, cuando contiene una acción compuesta, utilizará el diagrama sintáctico siguiente: if ( expresión lógica ) { instrucción } EJEMPLO 3.9. A partir de dos números reales introducidos por teclado, mostrar el resultado de la división si el divisor es distinto de 0. ANÁLISIS: a) Datos de entrada: ddo: Dividendo. Teclado. dsor: Divisor. Teclado. b) Datos de salida: coc (sólo si dsor ≠ 0): Cociente de la división. Monitor. DISEÑO: a) Parte declarativa: VARIABLES coc,ddo,dsor:real 32 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C division BLOCK escribir ("Dividendo y divisor:") leer (ddo,dsor) if then dsor ≠ 0 BLOCK coc ← ddo/dsor escribir(coc) CODIFICACIÓN: /***************************/ /* Cálculo de una división */ /***************************/ #include <stdio.h> int main() { float ddo,dsor,coc; } printf("Introduzca el dividendo y el divisor: "); scanf("%f %f",&ddo,&dsor); if (dsor != 0) /* solo se calcula si el divisor no es cero */ { coc=ddo/dsor; printf("El resultado de la división es %.2f\n",coc); } return 0; ESTRUCTURA SELECTIVA DOBLE (IF-THEN-ELSE) La estructura anterior es inadecuada para representar la selección de alternativas cuando se requiere que el algoritmo seleccione entre dos posibles acciones en función del resultado de una condición, de forma que si la condición se cumple se seleccione una alternativa y en caso contrario se seleccione la otra. La estructura selectiva doble nos permite representar más adecuadamente esta situación. Por lo tanto, una estructura selectiva doble deberá emplearse cuando se presenten dos alternativas de actuación mutuamente excluyentes que dependan del resultado de una misma condición. En un diagrama de Tabourier, esto se representará: if then else expresión lógica acción 1 acción 2 33 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C Si la expresión lógica toma valor verdadero, se ejecutará la acción 1; si toma valor falso, se ejecutará la acción 2. Al igual que en las estructuras selectivas simples, las acciones de una estructura selectiva doble pueden ser compuestas, en cuyo caso se representarán del modo descrito anteriormente para la estructura selectiva simple. En C, el diagrama sintáctico correspondiente a la estructura selectiva doble es el siguiente: expresión bloque de bloque de ) else lógica sentencias sentencias donde bloque de sentencias se refiere de forma genérica a una única acción simple o a una acción compuesta, aplicándose en este último caso el diagrama sintáctico para acciones compuestas estudiado anteriormente. if ( EJEMPLO 3.10. Indicar si, dado un número por teclado, este es par o impar. ANÁLISIS: a) Datos de entrada: n: Número entero. Teclado. b) Datos de salida: paridad: Mensaje indicando la paridad del número n. Monitor DISEÑO: a) Parte declarativa: VARIABLES n: entero paridad BLOCK escribir ("Un nº:") leer (n) if then else resto(n,2)=0 escribir ("Es par") escribir ("Es impar") CODIFICACIÓN: /***************************************/ /* Muestra si un número es par o impar */ /***************************************/ #include <stdio.h> int main() { int n; } printf("Introduzca un scanf("%d",&n); if (n % 2 == 0) /* printf("El número else printf("El número return 0; número entero: "); un número es par si al dividir entre 2 da resto 0 */ es PAR\n"); es IMPAR\n"); 34 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C ESTRUCTURA SELECTIVA MÚLTIPLE (SWITCH) A menudo, en la resolución de un problema se presentan más de dos alternativas que dependen de una misma circunstancia, por ejemplo, del día de la semana en el que nos encontramos. Esto podría resolverse usando estructuras selectivas dobles encadenadas, pero generaría un código poco legible y de difícil escritura si el número de alternativas es grande. La estructura selectiva múltiple permite seleccionar una alternativa de entre varias posibles, en función del resultado de una expresión de tipo entero o de tipo carácter. En un diagrama de Tabourier, esta estructura se representa de la siguiente forma: switch lista 1 expresión lista n lista 2 acción 1 acción 2 default acción n ... acción d Las listas lista 1, lista 2, ..., lista n contendrán el o los valores asociados a la alternativa correspondiente, separados por comas si son varios. La cláusula default es opcional y se usará cuando una alternativa englobe a los valores de la expresión que no se han indicado explícitamente. En C, el diagrama sintáctico de la estructura selectiva múltiple es: switch ( expresión ) { case expresión : bloque de sentencias break; default bloque de sentencias } Cada valor de una misma lista se representa mediante un par case-expresión acabado en dos puntos (:). Tras la lista, se sitúa el bloque de sentencias correspondiente a la acción asociada. Obsérvese que se utiliza una instrucción break; para separar cada par lista-acción de la siguiente y, si está presente, del par default-acción. EJEMPLO 3.11. Se desea diseñar un algoritmo que escriba por pantalla la duración en días correspondiente al mes cuyo número de orden se indique por teclado o un mensaje de error si dicho valor no está en el intervalo [1,12]. ANÁLISIS: a) Datos de entrada: mes: Número de orden del mes. Teclado. b) Datos de salida: dias: Mensaje indicando el número de días que tiene mes o un mensaje de error. Monitor DISEÑO: a) Parte declarativa: VARIABLES mes:entero 35 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C duracionMes BLOCK escribir ("Mes:") leer (mes) switch 1,3,5,7,8,10,12 mes escribir ("31 días") 4,6,9,11 default 2 escribir ("30 días") escribir ("28 o 29 días") escribir ("Error") CODIFICACIÓN: /****************************************************************************/ /* Muestra la duración en días correspondiente al número de orden de un mes */ /****************************************************************************/ #include <stdio.h> int main() { int mes; } printf("Introduzca el número de orden del mes: "); scanf("%d",&mes); switch (mes) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: printf("31 días\n"); break; case 4: case 6: case 9: case 11: printf("30 días\n"); break; case 2: /* depende de si el año es o no bisiesto */ printf("28 o 29 días\n"); break; default: printf("Error: el valor no corresponde a ningún mes.\n"); } return 0; 3.4.3 ESTRUCTURAS REPETITIVAS Otro tipo de estructura de control es aquélla que describe la repetición de una acción un número de veces a priori determinado o indeterminado. Estas estructuras se denominan estructuras repetitivas o bucles y se llama iteración a cada una de las ejecuciones de la acción en el bucle. El conjunto de pasos que constituye la acción que se repite se denomina cuerpo del bucle. Aparte del cuerpo del bucle, será necesario también establecer una condición de parada que determine el número de veces que el cuerpo del bucle se ejecutará (el número de iteraciones). Esta condición se localizará al comienzo o al final de bucle y podrá indicarse de distintos modos, dando lugar a tres tipos distintos de estructuras repetitivas: while-do, do-while y for-do. 36 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C ESTRUCTURA REPETITIVA WHILE-DO En ella, el cuerpo del bucle se repite mientras se cumpla una determinada condición. Su representación en un diagrama de Tabourier es la siguiente: while do expresión lógica acción Cuando se ejecuta la estructura while-do, primero se evalúa la expresión lógica. Si es falsa, termina la ejecución del bucle. Si la expresión lógica es verdadera, entonces se ejecuta el cuerpo del bucle, después de lo cual el flujo de ejecución vuelve hacia atrás para evaluar de nuevo la expresión lógica. El cuerpo del bucle continuará ejecutándose mientras la expresión lógica sea verdadera. Esta capacidad de “retroceder” en el flujo de ejecución es exclusiva de las estructuras repetitivas. Obsérvese que para que un bucle while-do finalice es necesario que la condición de parada se haga falsa. Por ello, en el cuerpo del bucle debe existir alguna instrucción que altere en cierto momento alguno de los operandos que intervienen en la condición. En caso contrario, si la condición se cumpliera en su primera evaluación también se cumpliría en el resto y, por tanto, nos encontraríamos ante un bucle infinito. La estructura repetitiva while-do deberá utilizarse cuando se desconozca a priori el número de iteraciones y no se requiera ejecutar el cuerpo del bucle, al menos, una vez. En C, se utiliza la instrucción while para implementar la estructura repetitiva while-do. Su diagrama sintáctico es el siguiente: while ( expresión lógica ) bloque de sentencias EJEMPLO 3.12. Sumar una serie de números no negativos introducidos por teclado hasta que se introduzca uno negativo y mostrar el total por pantalla. ANÁLISIS: a) Datos de entrada: n[k]: Secuencia de valores no negativos (excepto el último, que será negativo). Teclado. b) Datos de salida: tot: Suma de los valores no negativos de la secuencia. Monitor. c) Comentarios: Para realizar la suma no será necesario recordar todos los valores introducidos, sino que basta con ir recordando el total acumulado. Por lo tanto, emplearemos una variable acumuladora para almacenar dicho total y otra para almacenar el número actual. DISEÑO: a) Parte declarativa: VARIABLES n,tot:entero 37 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C b) Representación algorítmica: contar BLOCK tot ← 0 escribir ("Un nº:") leer(n) escribir (tot) while do n≥0 BLOCK tot ← tot+n escribir ("Otro nº:") leer(n) CODIFICACIÓN: /************************************************************/ /* Muestra la suma de una secuencia de valores no negativos */ /************************************************************/ #include <stdio.h> int main() { int n,tot; } tot=0; printf("Introduzca un número: "); scanf("%d",&n); while (n >= 0) { tot=tot+n; /* Operación de acumulación */ printf("Introduzca otro número: "); scanf("%d",&n); } printf("La suma es: %d\n",tot); return 0; ESTRUCTURA REPETITIVA DO-WHILE En la estructura do-while, de nuevo, el cuerpo del bucle se ejecuta mientras se cumpla una determinada condición. La diferencia respecto a la estructura while-do estriba en que la evaluación de la condición de parada se hace tras la ejecución del cuerpo del bucle, y no antes. Su representación en un diagrama de Tabourier es: do while acción expresión lógica Por tanto, una estructura do-while comienza ejecutando el cuerpo del bucle. A continuación se evalúa la expresión lógica. Si el resultado es falso, el bucle termina; si es verdadero, el cuerpo del bucle se ejecuta otra vez y a continuación la condición se evalúa de nuevo. La ejecución del cuerpo del bucle se repetirá mientras la condición sea verdadera. Obsérvese que, en una estructura do-while, el cuerpo del bucle se ejecuta al menos una vez. La estructura repetitiva do-while deberá utilizarse cuando el número de iteraciones sea desconocida a priori y se requiera ejecutar el cuerpo del bucle, al menos, una vez. 38 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C En C, la estructura do-while se implementa mediante el siguiente diagrama sintáctico: expresión bloque de while ( ) ; lógica sentencias A diferencia del resto de instrucciones de control, la instrucción do-while acaba en punto y coma. do EJEMPLO 3.13. Leer desde teclado una secuencia creciente de valores enteros. La secuencia finalizará cuando se introduzca un valor menor que el anterior. ANÁLISIS: a) Datos de entrada: n[k]: Secuencia de valores creciente, excepto el último valor, que será menor que el anterior. Teclado. b) Datos de salida: c) Comentarios: Para comprobar si la secuencia es creciente no será necesario recordar todos los valores introducidos, sino que basta con recordar el inmediatamente anterior y compararlo con el actual. Por lo tanto, emplearemos una variable para almacenar el número actual y otra para almacenar el inmediatamente anterior. DISEÑO: a) Parte declarativa: VARIABLES n,ant:entero creciente BLOCK escribir ("Un nº:") leer(n) do while BLOCK ant ← n escribir ("Otro nº:") n >= ant leer(n) CODIFICACIÓN: /********************************************************/ /* Lee desde teclado una secuencia de valores creciente */ /********************************************************/ #include <stdio.h> int main() { int n,ant; } printf("Introduzca un número: "); scanf("%d",&n); do { ant=n; /* el valor actual se almacena como anterior */ printf("Introduzca otro número: "); scanf("%d",&n); } while (n >= ant); /* continúa mientras la secuencia sea creciente */ return 0; 39 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C ESTRUCTURA REPETITIVA FOR-DO En ocasiones se conoce a priori el número de iteraciones de un bucle, esto es, justo antes de que este comience a ejecutarse. En esos casos, debe emplearse la estructura repetitiva for-do, cuya representación en un diagrama de Tabourier es la siguiente: for do id ← Vi ,Vf ,inc acción En el diagrama, id es una variable que controla de modo automático el número de iteraciones del bucle y se denomina variable índice. El valor inc podrá ser un entero positivo o negativo y provocará, respectivamente, un incremento o decremento de la variable índice tras cada iteración. La ejecución de la estructura for-do comienza asignando el valor inicial Vi a la variable índice. A continuación comprueba si el valor de la variable índice supera al valor final Vf. Si es así, la ejecución del bucle finaliza; en otro caso, la ejecución del cuerpo del bucle se repetirá hasta que la variable índice sobrepase al valor final Vf, la cual se incrementará/decrementará automáticamente en el valor de inc después de cada iteración. El valor de la variable índice queda indefinido tras finalizar el bucle, por lo que no se deberá suponer ningún valor para la misma en acciones posteriores al bucle. En C, la estructura for-do se implementa con la instrucción for empleando el siguiente diagrama sintáctico: bloque de sentencias Tanto Vi como Vf serán valores de tipo entero, expresados mediante literales, identificadores o expresiones aritméticas. La variable índice id también será de tipo entero. Si el incremento es negativo, en lugar del operador <= se utilizará el operador >=. for ( id = Vi; id <= Vf; id = id+inc ) EJEMPLO 3.14. Calcular la suma de los 100 primeros números naturales. ANÁLISIS: a) Datos de entrada: NUMMIN=0. Primer número natural. Dato fijo. NUMMAX=99. Centésimo número natural. Dato fijo. b) Datos de salida: tot: suma de los 100 primeros números naturales. Monitor. c) Comentarios: Se utilizará una variable para contar el número de valores sumados. DISEÑO: a) Parte declarativa: CONSTANTES NUMMIN=0 NUMMAX=99 VARIABLES tot,i:entero 40 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C suma100 BLOCK tot ← 0 i ← NUMMIN, NUMMAX,1 for do escribir (tot) tot ← tot+i CODIFICACIÓN: /*********************************************************/ /* Muestra la suma de los 100 primeros números naturales */ /*********************************************************/ #include <stdio.h> #define NUMMIN 0 #define NUMMAX 99 int main() { int tot,i; } /* Primer número natural */ /* Centésimo número natural */ tot=0; for (i=NUMMIN; i <= NUMMAX; i=i+1) tot=tot+i; printf("La suma es %d\n",tot); return 0; 41 3. LA REPRESENTACIÓN GRÁFICA DE LOS ALGORITMOS Y SU TRADUCCIÓN A C EJERCICIOS 1º) En el supermercado “El 13" las ventas no van muy bien. Por ello, han decidido lanzar una campaña de captación de clientes consistente en aplicar un descuento de un 5% sobre el importe de la compra si este supera los 60 euros y un 5% adicional si, además, dicho importe (despreciando céntimos de euro) es divisible entre 13. Desarrollar un programa que lea por teclado el importe inicial de una compra y calcule y muestre por pantalla el descuento aplicado y el importe final. 2º) Calcular y mostrar por pantalla la estatura del individuo más alto y más bajo de una serie de 100 estaturas introducidas por teclado. 3º) Se proporcionan por teclado las calificaciones de un examen (entre 0 y 10). Diseñar un algoritmo que muestre por pantalla la media de la clase y el número de aprobados (calificaciones superiores o iguales a 5). La introducción de calificaciones terminará cuando se teclee el valor -1. 4º) Calcular el producto de dos valores enteros no negativos introducidos por teclado, teniendo en cuenta que sólo podrá emplearse la operación de suma. 5º) Escribir en pantalla todos los números primos entre 2 y 10000, ambos inclusive. 6º) El desarrollo de la serie de Maclaurin para el logaritmo neperiano es: ln ( x ) = ( x−1) − ( x−1)2 ( x−1)3 ( x−1)4 ( x−1)5 + − + − ... , 2 3 4 5 (0< x≤2) Escribir un programa que evalúe y muestre por pantalla el valor de la serie con n términos, donde x y n se introducen por teclado. 7º) Calcular la división entera de dos valores enteros no negativos introducidos por teclado teniendo en cuenta que sólo podrán emplearse las operaciones de suma y resta. 42 4 ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES En capítulos anteriores se ha estudiado el concepto de datos de tipo simple (entero, real y carácter). A veces, los datos a tratar en un programa no son elementos individuales de información, sino que existe cierta relación entre ellos. En esos casos, los lenguajes de programación permiten trabajar con estructuras de datos, es decir, colecciones de datos más simples con relaciones establecidas entre ellos. Estas estructuras de datos posibilitan, por un lado, representar la información de una manera más natural y clara y, por otro, un tratamiento de la información más cómodo y eficiente. En este capítulo se tratarán dos de las estructuras de datos más habituales en programación: los arrays y las cadenas de caracteres. 4.1 INTRODUCCIÓN Una estructura de datos es una colección de datos caracterizada por su organización y por las operaciones definidas sobre ella. Los tipos de datos utilizados para declarar estructuras de datos se denominan tipos compuestos y se construyen a partir de los tipos simples ya estudiados. Distinguimos dos categorías de estructuras de datos: Estáticas: su tamaño se determina a priori, antes del comienzo de la ejecución del programa, y este no podrá incrementarse ni disminuirse en tiempo de ejecución. Esto implicará que cuando se trabaje con una estructura de datos estática cuyo tamaño se desconoce en la fase de diseño, será necesario establecer un tamaño máximo y reservar espacio en memoria para ese máximo (con el posible desperdicio de memoria que esto pueda conllevar). Entre las estructuras de datos estáticas distinguimos arrays, cadenas de caracteres y registros. Dinámicas: su tamaño se determina en tiempo de ejecución, reservando y liberando espacio en memoria en el momento que interese. Esto permitirá, por tanto, optimizar al máximo el espacio de ocupación en memoria, aunque requiere una gestión de memoria más complicada. Se distinguen listas, árboles y grafos. En esta asignatura nos centraremos en las estructuras de datos estáticas, adecuadas como una primera aproximación al manejo de estructuras de datos por ser más sencillas de gestionar. Una característica común a todas las estructuras de datos es la existencia de un único identificador que hace referencia a la misma en su conjunto. Además, cada tipo de estructura de 43 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES datos dispone de su propio mecanismo para hacer referencia de forma independiente a los elementos que la integran. 4.2 ARRAYS Un array es una colección de elementos de un mismo tipo, donde cada elemento puede identificarse por su posición dentro de la estructura. Todo array posee un identificador que lo designa y una serie de índices que toman valores enteros y permiten diferenciar por su posición a los distintos elementos que lo constituyen. 4.2.1 ARRAYS UNIDIMENSIONALES Un array unidimensional puede considerarse como una lista ordenada de valores. Lleva asociado un único índice que designa la posición de los valores dentro de la lista, comenzando en lenguaje C desde 0. Debido a su similitud con el concepto matemático de vector, los arrays unidimensionales también se conocen con el nombre de vectores. En la siguiente figura se representa gráficamente un vector que representa el número de habitantes (en unidades de millar) de 100 poblaciones: habitantes 0 30 1 7 2 5 99 120 ... Para hacer referencia a cada elemento del vector, tanto en algoritmia como en C, se utiliza la siguiente sintaxis: identificador[índice] Por ejemplo, el número de habitantes de la tercera población se designa habitantes[2] y su contenido es igual a 5. En algoritmia, la declaración de estructuras de datos de tipo vector utiliza el siguiente diagrama sintáctico: VARIABLES identificador de variable valor entero [ tipo : ] , El valor entero representa el número de elementos del vector y, por tanto, el rango de valores que puede tomar el índice (en C, desde 0 a valor entero – 1, ambos inclusive). La traducción a C de esta declaración se realiza según el siguiente diagrama sintáctico: tipo identificador de variable [ valor entero ] ; , 44 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES Por ejemplo, las declaraciones del vector habitantes en algoritmia y en C emplearían la siguiente sintaxis: En algoritmia: En C: VARIABLES habitantes[100]:entero int habitantes[100]; En C, los conceptos de vector y puntero están relacionados. Concretamente, el identificador de un vector, sin especificar el índice, representa la dirección de memoria donde comienza el vector (es decir, la dirección del primer elemento del vector). Así, en el ejemplo anterior, habitantes representa la dirección de memoria donde se almacena habitantes[0] y, por tanto, habitantes==&habitantes[0] o, expresado de otro modo *habitantes==habitantes[0]==30 Es más, cuando un puntero p apunta a un elemento de un vector, puede utilizarse la sintaxis p+n (donde n es un entero) para apuntar al elemento situado n posiciones a continuación de la apuntada por p. Por ejemplo, dado que habitantes es un puntero que apunta al primer elemento del vector habitantes, habitantes+2 equivale a la dirección del tercer elemento del vector habitantes. Así, habitantes+2==&habitantes[2] o, expresado de otro modo *(habitantes+2)==habitantes[2]==5 EJEMPLO 4.1. A partir de las edades de 10 individuos introducidas por teclado, calcular y mostrar cuántos son mayores y cuántos menores que la media. ANÁLISIS: a) Datos de entrada: NUMIND=10. Número de individuos. Dato fijo. edad[NUMIND]: Edades de los individuos. Teclado. (edad[i] ≥ 0, ∀i) b) Datos de salida: may: Número de individuos con edad superior a la media de edad[NUMIND]. Monitor. men: Número de individuos con edad inferior a la media de edad[NUMIND]. Monitor. c) Comentarios: Dado que, una vez calculada la media de las edades, será necesario recorrer de nuevo la lista de edades, se utilizará un vector para representar dicha lista. Se utilizará una variable índice para recorrer el vector, en primer lugar, para almacenar los valores en él durante la lectura desde teclado y el cálculo de la media y, en segundo lugar, para determinar cuántos de esos valores son mayores y cuántos menores que la media. Se utilizará una variable para acumular los valores del vector durante el cálculo de la media. Se utilizará una variable para almacenar el resultado de la media. DISEÑO: a) Parte declarativa: CONSTANTES NUMIND=10 VARIABLES edad[NUMIND]:entero med:real i,suma,may,men:entero 45 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES b) Representación algorítmica: contar BLOCK suma←0 for do med←suma/NUMIND men←0 may←0 for do BLOCK i ← 0,NUMIND-1,1 escribir leer ("Edad",i+1) (edad[i]) while do edad[i] < 0 escribir ("Error") if then else i ← 0,NUMIND-1,1 suma←suma+edad[i] edad[i] < med BLOCK escribir (may,men) men←men+1 edad[i] > med if then may←may+1 escribir leer ("Edad",i+1) (edad[i]) CODIFICACIÓN: /*************************/ /*** E D A D E S ***/ /*************************/ #include <stdio.h> #define NUMIND 10 /* Número de individuos (edades) */ int main() { int edad[NUMIND]; double med; int i,suma,may,men; /* Lectura desde teclado del vector y cálculo de la media */ suma=0; for (i=0; i <= NUMIND-1; i=i+1) { printf("Edad %d: ",i+1); scanf("%d",&edad[i]); while (edad[i] < 0) { printf("Error, debe ser mayor o igual que cero.\n"); printf("Edad %d: ",i+1); scanf("%d",&edad[i]); } suma=suma+edad[i]; } med=suma/NUMIND; /* Cálculo de número de valores superiores e inferiores a la media */ men=0; may=0; for (i=0; i <= NUMIND-1; i=i+1) if (edad[i] < med) men=men+1; else if (edad[i] > med) may=may+1; } /* Visualización de resultados */ printf("Número de individuos mayores que la media: %d.\n",may); printf("Número de individuos menores que la media: %d.\n",men); return 0; 46 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES 4.2.2 ARRAYS MULTIDIMENSIONALES A veces, existen datos cuya representación es más adecuada en forma de tabla con dos o más índices, por ejemplo, para tratar la disposición de las fichas en un tablero de ajedrez (8×8 casillas) o valores diarios durante los meses de una década (31×12×10 valores). Para ello, se pueden emplear arrays multidimensionales. Un array bidimensional (también denominado matriz) puede considerarse como un array unidimensional cuyos elementos son vectores. Así, por ejemplo, la representación del censo de habitantes de 100 poblaciones en las 3 últimas décadas podría efectuarse mediante una matriz de dimensiones 100×3, como se muestra en la siguiente figura: Índice de década 0 1 2 31 28 30 10 8 7 ... ... ... 60 90 120 habitantes Índice de población 0 1 ... 99 Para referenciar a cada elemento de un array bidimensional se utilizan dos índices. El primero se refiere a la fila y el segundo a la columna que ocupa dicho elemento. Se utiliza la siguiente sintaxis: identificador[fila][columna] Por ejemplo, el número de habitantes de la segunda población en el censo de la primera década se designa habitantes[1][0] y su contenido es igual a 10. Análogamente, pueden declararse arrays de tantas dimensiones como se quiera, teniendo como limitación el tamaño de la memoria del ordenador. El número total de elementos del array es el producto del número de elementos de cada dimensión. Por ejemplo, un array de dimensión 3×10×2 tendrá 60 elementos. No obstante, obsérvese que a mayor número de dimensiones, menor será la legibilidad de la solución y mayor la dificultad de su manejo, pues cada índice hará referencia a una característica de los datos y debemos saber en qué orden debe situarse cada uno de los índices (por ejemplo, primero el código de población, a continuación la década, etc.). En general, un elemento de un array n-dimensional se referencia con la siguiente sintaxis: identificador[índice1][índice2]...[índicen] Al igual que en el caso de los vectores, todos los elementos de un array multidimensional deben ser de igual tipo. Si junto con el número de habitantes del ejemplo anterior quisiéramos almacenar el nombre de la entidad que realizó el censo (representado por un dato de tipo cadena de caracteres), sería preciso declarar una nueva matriz de tamaño 100×3 en vez de añadir una nueva dimensión al array original, ya que los datos a almacenar son de distinto tipo1. En algoritmia, la declaración de una estructura de datos array multidimensional utiliza el siguiente diagrama sintáctico: VARIABLES identificador de variable valor entero [ ] : tipo , 1 Veremos en el Capítulo 6 que existe una alternativa más adecuada mediante el uso de registros. 47 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES Ahora, cada valor entero representa el número de elementos de la dimensión correspondiente y, por tanto, el rango de valores que puede tomar su índice (en C, desde 0 a valor entero – 1). La traducción a C de esta declaración se realiza según el siguiente diagrama sintáctico: tipo identificador de variable valor entero [ ] ; , Por ejemplo, la declaración de la matriz habitantes vista anteriormente emplearía la siguiente sintaxis: En algoritmia: En C: int habitantes[100][3]; VARIABLES habitantes[100][3]:entero EJEMPLO 4.2. Almacenar desde teclado valores reales en una matriz de dimensiones 20x20 por filas y mostrar por pantalla la suma de sus columnas. ANÁLISIS: a) Datos de entrada: NUMFIL=5. Número de filas. Dato fijo. NUMCOL=5. Número de columnas. Dato fijo. m[NUMFIL][NUMCOL]: Matriz de números reales. Teclado. b) Datos de salida: s[NUMCOL]: Sumas de las columnas de la matriz. Monitor. c) Comentarios: Se utilizarán dos variables índice para recorrer la matriz, en primer lugar, durante su lectura desde teclado y, en segundo, durante el cálculo de la suma de cada columna. Dado que se mostrará la suma de cada columna tras su cálculo, no es necesario recordar simultáneamente cada una de ellas, por lo que sólo se utilizará una variable para almacenarlas. DISEÑO: a) Parte declarativa: CONSTANTES NUMFIL=5 NUMCOL=5 VARIABLES m[NUMFIL][NUMCOL]:real i,j:entero s:real b) Representación algorítmica: sumaColumnas BLOCK for do i←0,NUMFIL-1,1 for do BLOCK escribir("Fila",i+1) j←0,NUMCOL-1,1 BLOCK j←0,NUMCOL-1,1 for do leer(m[i][j]) s←0 i←0,NUMFIL-1,1 for do escribir(s) s←s+m[i][j] 48 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES CODIFICACIÓN: /***************************************/ /*** S U M A C O L U M N A S ***/ /***************************************/ #include <stdio.h> #define NUMFIL 5 /* Número de filas de la matriz */ #define NUMCOL 5 /* Número de columnas de la matriz */ int main() { double m[NUMFIL][NUMCOL],s; int i,j; /* Lectura desde teclado de la matriz por filas */ for (i=0; i <= NUMFIL-1; i=i+1) { printf("Fila %d: ",i+1); for (j=0; j <= NUMCOL-1; j=j+1) scanf("%lf",&m[i][j]); } } /* Cálculo de la suma de cada columna y visualización de resultados */ for (j=0; j <= NUMCOL-1; j=j+1) { s=0; for (i=0; i <= NUMFIL-1; i=i+1) s=s+m[i][j]; printf("La suma de la columna %d es %.2f.\n",j+1,s); } return 0; 4.3 CADENAS DE CARACTERES En el desarrollo de programas, a menudo es necesario tratar información alfabética, por ejemplo, para la creación y gestión de listas de personal, inventarios, etc. El tipo de datos utilizado para representar esta información se denomina cadena de caracteres o, simplemente, cadena. Se llama longitud de una cadena al número de caracteres que contiene. La cadena que no contiene ningún carácter se denomina cadena vacía o nula y su longitud es cero. Para declarar una variable nombre de tipo cadena con una longitud máxima de 14 caracteres significativos utilizaremos la siguiente sintaxis: En algoritmia: VARIABLES nombre[15]:carácter En C: char nombre[15]; donde el último elemento de la cadena se reserva para el carácter especial '\0' (carácter nulo) que representa el fin de la cadena. 49 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES 4.3.1 OPERACIONES CON CADENAS Una cadena de caracteres no es más que un vector de caracteres delimitado por el carácter nulo. Por ello, en la mayoría de los casos, podemos operar con las cadenas del mismo modo que con cualquier otro vector. No obstante, existe una biblioteca de funciones en C (con archivo de cabecera string.h) que permiten manejar las cadenas de caracteres de un modo más cómodo. En esta sección estudiaremos algunas de ellas. ASIGNACIÓN Al igual que con cualquier otro vector, para almacenar en una variable de tipo cadena una cadena de caracteres, el uso del operador de asignación requeriría asignar uno a uno los caracteres que componen la cadena al elemento correspondiente de la variable mediante el uso de una estructura repetitiva for do. En lugar de ello, puede realizarse la misma asignación más cómodamente utilizando la función interna strcpy. Esta función emplea dos parámetros: el primero, la variable cadena que recibirá la asignación, y el segundo, la cadena a asignar. Por ejemplo, dada la variable nombre declarada en la sección anterior, la instrucción de C strcpy(nombre,"1234567890"); almacenará en dicha variable el valor "1234567890". La función strcpy copia todos los caracteres contenidos en el segundo parámetro hasta encontrar el carácter nulo. Por ello, el programador debe asegurarse de reservar suficiente espacio durante la declaración de la variable que va a albergar el contenido que se le asigna. Por ejemplo, la instrucción strcpy(nombre,"123456789012345"); no sería correcta, ya que se están intentando asignar 16 caracteres (15 significativos más el carácter nulo) a una cadena para la que solo se han reservado 15. En algoritmia, nos referiremos a esta operación con el operador de asignación ←. LECTURA Y ESCRITURA Aunque una cadena de caracteres es un vector de caracteres y podemos hacer referencia a sus caracteres individuales utilizando la notación de vectores, a diferencia de otros tipos de vectores, para leer por teclado o escribir en pantalla una cadena de caracteres basta con incluir el nombre de la variable entre los paréntesis de una instrucción gets o printf, respectivamente, tal y como se estudió en el Capítulo 3. COMPARACIÓN La comparación de cadenas posee gran importancia en la ordenación de datos alfabéticos, búsqueda de textos, etc. El criterio de clasificación se basa en el orden numérico de los caracteres según su código de ASCII. Dos cadenas serán iguales si contienen exactamente los mismos caracteres situados en el mismo orden. En otro caso, el resultado de la comparación será el del primer par de caracteres distintos situados en una misma posición. En este sentido, la presencia de cualquier carácter se considerará siempre mayor que la ausencia de carácter. 50 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES En C, para comprobar si dos cadenas son iguales se utilizará la función interna strcmp, que tiene como parámetros las dos cadenas de caracteres a comparar. El valor devuelto por la función tiene el siguiente significado: Un valor positivo significa que la primera cadena es mayor (sucede alfabéticamente) a la segunda. Un valor igual a cero significa que ambas cadenas son idénticas. Un valor negativo significa que la primera cadena es menor (precede alfabéticamente) a la segunda. Ejemplos de comparaciones son: strcmp("norte","norte") strcmp("ciber","ciudad") strcmp("DATOS ","DATOS") strcmp("lima","Lima") strcmp("casa","casaca") 0 negativo positivo positivo negativo En algoritmia, representaremos las comparaciones entre cadenas mediante los operadores relacionales (=, ≠, ≤, ≥, >, <). CÁLCULO DE LA LONGITUD Para calcular la longitud de una cadena en lenguaje C se utiliza la función interna strlen. Como parámetro se especificará un valor de tipo cadena. Devolverá un valor entero igual a la longitud actual de la cadena (sin contar el carácter nulo). En algoritmia nos referiremos a esta operación con la palabra longitud. CONCATENACIÓN La concatenación consiste en la unión de varias subcadenas en una única cadena. En algoritmia, utilizaremos el operador de suma (+) para representar la concatenación. Por ejemplo, "PARA"+"BRISAS" = "PARABRISAS" Puede observarse que se unen las cadenas sin insertar espacios en blanco entre ellas, por lo que estos deberán incluirse explícitamente en alguna de las cadenas a concatenar si se desea que permanezcan separadas. En lenguaje C, la concatenación se consigue mediante la función interna strcat, que añade una cadena al final de otra cadena. Utiliza dos parámetros: el primero, la variable cadena destino de la concatenación, y el segundo, la cadena que se añade. Por ejemplo, dada la variable nombre de secciones anteriores, tras las siguientes dos instrucciones: strcpy(nombre,"JOSE"); strcat(nombre,"MANUEL"); la variable nombre contendrá el valor "JOSEMANUEL". 51 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES BÚSQUEDA La operación de búsqueda consiste en localizar una determinada cadena como parte de otra cadena más grande. En C, la función que realiza este cometido es strstr, que utiliza dos parámetros de tipo cadena: el primero, la cadena donde se buscará; el segundo, la subcadena a localizar. El valor devuelto es un puntero a la subcadena buscada en la primera aparición dentro de la cadena explorada. Si la subcadena no aparece en la cadena, el valor devuelto será NULL. En la representación algorítmica se utilizará la palabra buscar. Por ejemplo, dada la cadena quijote="En un lugar de la Mancha de..." strstr(quijote,"de") En un lugar de la Mancha de... ^ strstr(quijote,"e") En un lugar de la Mancha de... ^ strstr(quijote,"arde") devuelve el valor NULL EJEMPLO 4.3. Desarrollar un programa que lea dos cadenas de máximo 100 caracteres y muestre el número de veces que la segunda aparece incluida en la primera, sin contabilizar solapamientos. ANÁLISIS: a) Datos de entrada: LONMAX=100: Longitud máxima de las cadenas. Dato fijo. cad[LONMAX+1]: Cadena a examinar. Teclado sub[LONMAX+1]: Subcadena a buscar. Teclado b) Datos de salida: rep: Número de veces que la subcadena sub aparece en la cadena cad (sin solapamientos). Monitor. c) Comentarios: Se empleará una variable para contar el número de ocurrencias de la subcadena en la cadena. Debido a que la función strstr sólo busca la primera ocurrencia de una subcadena, habrá que ir desechando la parte de la cadena ya inspeccionada. Por ello, se utilizará un puntero que apunte a cada ocurrencia de la subcadena, lo que nos permitirá determinar qué parte de la cadena queda por explorar. DISEÑO: a) Parte declarativa: CONSTANTES LONMAX=100 VARIABLES cad[LONMAX+1],sub[LONMAX+1],*p:carácter rep:entero b) Representación algorítmica: repeticiones BLOCK escribir leer rep←0 ("Cadenas:") (cad,sub) p←buscar(cad,sub) p ≠ NULL while do escribir(rep) BLOCK rep←rep+1 cad←p+longitud(sub) p←buscar(cad,sub) 52 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES CODIFICACIÓN: /***************************************/ /*** R E P E T I C I O N E S ***/ /***************************************/ #include <stdio.h> #include <string.h> #define LONMAX 100 /* Longitud máxima de las cadenas */ int main() { char cad[LONMAX+1],sub[LONMAX+1],*p; int rep; } printf("Introduce la cadena a inspeccionar: "); fflush(stdin); gets(cad); printf("Introduce la subcadena a buscar: "); fflush(stdin); gets(sub); rep=0; p=strstr(cad,sub); while (p != NULL) /* Mientras se encuentre sub en cad */ { rep=rep+1; strcpy(cad,p+strlen(sub)); p=strstr(cad,sub); } printf("La subcadena aparece %d veces en la cadena\n",rep); return 0; 53 4. ESTRUCTURAS DE DATOS (I): ARRAYS. CADENAS DE CARACTERES EJERCICIOS 1º) Desarrollar un programa que almacene desde teclado n números enteros en un vector e indique por pantalla si este es capicúa, siendo n un número entre 2 y 100 determinado por el usuario. La comprobación finalizará cuando se detecte una pareja de elementos simétricos distintos. 2º) Desarrollar un programa que almacene desde teclado 10 números enteros en un vector y a continuación indique por pantalla si el valor de algún elemento coincide con la suma de todos los que están a su izquierda en el vector. 3º) Diseñar un programa que lea desde teclado una matriz de 5×4 valores reales y a continuación muestre por pantalla su traspuesta. 4º) Desarrollar un programa que lea de teclado dos matrices de valores reales A y B de dimensiones 5×4 y 4×6, respectivamente, y muestre el resultado de A×B por pantalla. 5º) Se desea realizar un programa que, leyendo desde teclado una matriz de 6×6 valores reales positivos y dos valores a y b, muestre por pantalla la media de los elementos de la matriz que sean mayores que a y menores que b. 6º) Desarrollar un programa que lea una frase desde teclado (con un máximo de 200 caracteres) y a continuación muestre por pantalla el número de palabras que contiene la frase. 7º) En una versión electrónica del juego “Master Mind” el objetivo es averiguar una combinación de 5 dígitos (entre el 0 y el 9) sin repetición. Para ello, el usuario va proporcionando combinaciones y el ordenador responde informando sobre el número de JAQUES y de MATES entre la combinación secreta y la especificada por el usuario. El número de MATES equivale al número de dígitos entre ambas combinaciones que coinciden en valor y posición. El número de JAQUES equivale al número de dígitos entre ambas combinaciones que coinciden solamente en valor, pero no en posición. Desarrollar un programa que simule el juego del “Master Mind”. Para ello, el ordenador leerá en primer lugar la combinación secreta desde teclado y a continuación, tras borrar la pantalla, el usuario deberá introducir combinaciones hasta averiguar la combinación secreta, después de lo cual se mostrará el número de intentos empleados. 8º) Se considera que una frase está encadenada si la letra final de cada palabra coincide con la inicial de la siguiente. Desarrollar un programa que lea una frase desde teclado (con un máximo de 200 caracteres) y a continuación indique si está o no encadenada, suponiendo que las palabras están delimitadas por espacios y que la frase no contiene signos de puntuación. Además, la frase se considerará incorrecta si comienza o finaliza por espacios, contiene espacios consecutivos o está formada por una sola palabra. 54 5 MODULARIDAD Una vez conocidas las estructuras de control disponibles en la programación estructurada, es posible desarrollar cualquier programa. Sin embargo, a medida que los problemas se hacen más complejos, resulta más difícil su resolución como un todo, al tener que considerar simultáneamente todos los aspectos que afectan al problema. En este capítulo se introduce la modularidad, una técnica basada en la separación de problemas complejos en tareas más simples, simplificando su resolución y contribuyendo a mejorar la claridad de las soluciones alcanzadas. Además, se estudiarán los mecanismos disponibles en C para implementar esta técnica. 5.1 INTRODUCCIÓN A LA MODULARIDAD La resolución de un problema complejo puede simplificarse dividiendo a este en subproblemas más sencillos y a estos, a su vez, en otros más simples hasta que los más pequeños sean fáciles de resolver. Esta técnica, basada en la estrategia “divide y vencerás”, es empleada habitualmente en programación y se conoce con el nombre de modularidad. A cada uno de los subproblemas considerados se le denomina módulo. Para ilustrar esta idea, supóngase que se desea desarrollar un programa para el cálculo del IRPF. Acometer como un todo la resolución de este problema, de considerable envergadura, sería una tarea complicada y el programa resultante contendría probablemente una gran cantidad de código difícil de depurar y mantener. Sin embargo, este problema podría desglosarse en la solución sucesiva de tareas más simples, por ejemplo: lectura de los datos del contribuyente, cálculo de los resultados y visualización de los resultados. Cálculo IRPF Lectura datos Cálculo resultados Visualización resultados 55 5. MODULARIDAD Además, la lectura de datos del contribuyente podría aún desglosarse, por ejemplo, en lectura de datos personales y lectura de datos económicos: Cálculo IRPF Lectura datos Datos personales Cálculo resultados Visualización resultados Datos económicos Las ventajas de esta forma de resolver los problemas son numerosas: Facilita la depuración y el mantenimiento de programas. Así, en el ejemplo del cálculo del IRPF, un error en la lectura de la fecha de nacimiento del contribuyente podría buscarse directamente en el módulo Datos personales, mientras que una modificación de las escalas de gravamen afectaría únicamente al módulo Cálculo resultados. Facilita el trabajo en equipo, ya que la asignación de tareas puede hacerse de forma sencilla y, con una buena división modular, la independencia entre dichas tareas permitirá a cada individuo concentrarse en su trabajo sin preocuparse del método de resolución empleado para realizar las restantes tareas. A este respecto, debe señalarse que el diseño modular ha de buscar siempre la encapsulación de sus módulos, es decir, la ocultación de los aspectos internos de implementación del módulo (el cómo) al resto de módulos que lo utilizan, los cuales sólo requerirán conocer qué hace (el qué). Posibilita la reducción del tamaño de los programas, ya que un módulo puede ser utilizado en varias ocasiones dentro de un mismo programa, mientras que las instrucciones que describen la tarea del módulo sólo será necesario incluirlas una vez. Por ejemplo, en el cálculo del IRPF la lectura y validación de una fecha podría considerarse un módulo independiente. Si durante la lectura de los datos personales es necesario leer la fecha del contribuyente y la de su cónyuge, sólo será necesario repetir la llamada a dicho módulo y no la descripción de cómo se realiza la lectura y validación de una fecha. Favorece la reutilización del código, es decir, el aprovechamiento de resoluciones de subproblemas llevadas a cabo con anterioridad. El desarrollo de módulos que resuelven problemas frecuentes permitirá al programador recopilar en una biblioteca de funciones propia una batería de soluciones genéricas que incrementen su rendimiento futuro. Por ejemplo, un módulo que lea una fecha y la valide podría reutilizarse fácilmente en la resolución de otros problemas. Por lo tanto, la metodología empleada consiste en comenzar, desde una visión general del problema, determinando los subproblemas que la componen. Cada uno de estos se divide a continuación, si se estima necesario, en otros más simples. Como se ha visto, esto puede representarse como una estructura jerárquica en cuya parte superior se encuentra el problema principal y en la inferior los subproblemas más simples. Por esto, a esta metodología también se la denomina de diseño descendente. Cada nivel de esta estructura representa un grado de detalle y, por tanto, de abstracción, distinto. 56 5. MODULARIDAD Las soluciones de un diseño descendente pueden implementarse en C con el concepto de módulo. El módulo correspondiente al problema principal se denomina programa principal y el resto de módulos en los que se divide subprogramas. En lenguaje algorítmico utilizaremos los términos algoritmo principal y subalgoritmos. Normalmente, un módulo es llamado desde otro módulo situado en un nivel jerárquico superior al suyo. Tras la llamada, el flujo de ejecución se traslada a la primera instrucción del módulo llamado y comienza su ejecución. Cuando esta termina, el control es devuelto al lugar del módulo llamador desde el que el módulo fue invocado. Como se ha comentado, esto puede ocurrir en diferentes lugares del módulo llamador. A continuación se ilustra esa transferencia del flujo de ejecución en el ejemplo anterior. Calculo IRPF Llam ada leerDatos(...) datosPersonales(...) . . . datosPersonales(...) . . . datosEconomicos(...) . . . . . . leerFecha(...) . . . . leerFecha(...) . . . da ma Lla o rn to Re o rn to Re . . . leerDatos(...) . . calculoResultados(...) . . verResultados(...) . . leerFecha(...) 1 ada Llam 2 a ad m a Ll Re to rn o 1 Re torn o2 Instrucciones del módulo En C, el concepto de módulo se corresponde con el de función. A continuación, se detallan los aspectos de definición y uso de las mismas. 5.2 DEFINICIÓN DE FUNCIONES Matemáticamente, una función es una operación que toma uno o más valores llamados argumentos o parámetros y genera un resultado basándose en estos. Así, por ejemplo: f x , y = x 2 y 2 es una función cuyo nombre es f y cuyos parámetros son x e y. Obsérvese que en la definición de la función no se asocia ningún valor específico ni a x ni a y: diremos que x e y son los parámetros formales. Para evaluar la función y obtener un valor concreto es necesario asociar a los parámetros formales valores específicos que llamaremos parámetros reales. Así, por ejemplo, los parámetros reales 3 y 4 para x e y, respectivamente, permiten obtener el valor 5 de la función f. En el Capítulo 2 se presentaron algunas funciones que C lleva incorporadas, tales como funciones trigonométricas, de redondeo, etc. Estas funciones se denominan funciones internas. Cuando el tipo de cálculo deseado no lo cubren las funciones internas, es necesario recurrir a las funciones externas, que serán definidas por el programador mediante una definición de función. La definición de una función consiste en la descripción del modo en que esta realiza su cometido. Al igual que en el caso de un programa sin modularidad, la definición de una función se divide en tres fases: análisis, diseño y resolución en el ordenador. En la fase de análisis, la especificación de datos de entrada y datos de salida es semejante a la vista en capítulos anteriores. Sin embargo, ahora, los datos de entrada podrán proceder también del módulo llamador (en concreto, cuando se trate de los parámetros de la función). En ese caso, en la especificación del dato de entrada se indicarán también las restricciones que se asume que ya 57 5. MODULARIDAD cumple dicho dato cuando llega a la función. Igualmente, los datos de salida pueden tener como destino, no solamente el monitor, sino también el módulo llamador si son el resultado de la función. En la fase de diseño, en la parte declarativa y antes de la definición de constantes simbólicas y la declaración de variables de que haga uso la función, deberá indicarse la cabecera de la función, que contendrá los identificadores de los parámetros formales, el tipo asociado a cada uno de ellos y el tipo asociado al valor devuelto por la función, utilizando el siguiente diagrama sintáctico: identificador del módulo ( identificador de parámetro : tipo ) : tipo , El identificador del módulo se refiere al nombre de la función. A continuación, entre paréntesis, se incluirán los identificadores de los parámetros formales junto con sus tipos correspondientes (o únicamente los paréntesis si la función carece de parámetros). Por último, se indicará el tipo devuelto por la función. La representación algorítmica será muy similar a la vista en el Capítulo 3 para un algoritmo completo. La única diferencia es que en un subalgoritmo, el primer símbolo del que parten el resto de instrucciones será el que se indica a continuación: identificador_módulo(lista de parámetros) BLOCK La lista de parámetros se refiere a los parámetros formales que utiliza la función. Será, por lo tanto, una lista de identificadores separados por comas (sin indicar tipos). En cuanto a identificador_módulo será el nombre de la función. Además, para expresar en el subalgoritmo el valor que finalmente devolverá la función como resultado al módulo llamador, se empleará la siguiente representación: devolver(expresión) En la fase de resolución en el ordenador, en C la definición de una función se sitúa después del programa principal (función main()). Una función tiene una constitución similar al programa principal, es decir, posee una sección de definición de constantes simbólicas, una cabecera de función y un cuerpo que contiene la declaración de variables y las instrucciones correspondientes al diagrama de Tabourier. La cabecera, sigue el siguiente diagrama sintáctico: tipo identificador de la función ( tipo identificador de parámetro ) , Las secciones de definición de constantes simbólicas y declaración de variables contendrán, respectivamente, las constantes simbólicas y variables que la función utilice. El resto del cuerpo estará formado por el conjunto de instrucciones que llevan a la obtención del valor que la función debe devolver. La última instrucción del cuerpo de la función deberá ser de la forma: return expresión; indicando así el valor que esta devuelve como resultado al módulo llamador. 58 5. MODULARIDAD EJEMPLO 5.1. Desarrollar un módulo que calcule y devuelva el factorial de un número recibido como parámetro. ANÁLISIS: a) Datos de entrada: n: Valor del que quiere calcularse el factorial. Módulo llamador. (n ≥ 0) b) Datos de salida: factorial: Factorial de n. Módulo llamador. M. llamador f n factorial c) Comentarios: Emplearemos una variable para contar el número de multiplicaciones efectuadas. DISEÑO: a) Parte declarativa: factorial(n:entero):real VARIABLES i:entero f:real b) Representación algorítmica: factorial(n) BLOCK f←1 for do i ← 2, n, 1 devolver (f) f ← fi CODIFICACIÓN: /* factorial(n) */ /* Devuelve el factorial de un número n */ double factorial(int n) { int i; double f; f=1; for (i=2; i<=n; i=i+1) f=f*i; return f; } 5.3 INVOCACIÓN DE FUNCIONES Para emplear una función es necesario invocarla desde otro módulo. Dado que la invocación se sustituirá por el resultado devuelto, esta deberá formar parte de una expresión para que su valor no se pierda en el módulo llamador. La sintaxis que se utiliza es la siguiente: ...identificador_función(lista de parámetros reales)... La lista de parámetros se refiere a los parámetros reales y será una lista de expresiones separadas por comas. El número de parámetros reales incluidos en esta lista debe coincidir con el de parámetros formales indicados en la cabecera de la función durante su definición. Además, el tipo de cada parámetro real debe coincidir con el de su correspondiente parámetro formal o, en caso contrario, se producirá una conversión de tipo que puede provocar pérdida de información. 59 5. MODULARIDAD Una llamada a una función implica los siguientes pasos: 1. El control de la ejecución se transfiere del módulo llamador a la función. 2. A cada parámetro formal se le asigna el valor del parámetro real que ocupe su misma posición en la lista de parámetros. 3. Se ejecutan las instrucciones de la función. 4. El valor de la función es devuelto al módulo llamador junto con el control de la ejecución. 5. Se evalúa la expresión del módulo llamador que contiene a la invocación. El uso de módulos definidos por el programador también afecta a las distintas fases de resolución del módulo llamador. En la fase de análisis, deberá incluirse un comentario por cada uno de los módulos definidos por el programador que vayan a utilizarse, indicando su cometido y una descripción de la información que se transferirá desde y hacia el módulo llamador. En la fase de diseño, en la parte declarativa y tras la definición de constantes simbólicas, deberá indicarse la lista de módulos a los que se invoca, empleando para cada uno la siguiente sintaxis: identificador del módulo ( tipo ) tipo : , y precediendo a toda la lista del epígrafe MÓDULOS LLAMADOS. En la representación algorítmica, los pasos que contengan una invocación a un módulo definido por el programador estarán encerrados en el siguiente símbolo: Por último, en la fase de resolución en el ordenador deberá indicarse, inmediatamente después de la definición de constantes de los módulos que invoquen a funciones externas, los prototipos de dichas funciones, que deben seguir el siguiente diagrama sintáctico: tipo identificador de la función ( tipo ) ; , EJEMPLO 5.2. Calcular y mostrar por pantalla el número de combinaciones sin repetición de n elementos tomados de m en m, solicitando n y m por teclado. ANÁLISIS: a) Datos de entrada: n: número total de elementos. Teclado. (n > 0) m: tamaño de cada grupo de elementos tomado. Teclado. (n ≥ m > 0) M. P. n leerEntPos b) Datos de salida: result: número de combinaciones sin repetición de n elementos tomados de m en m. Monitor. n f factorial c) Comentarios: Se utilizará un módulo para leer un número positivo desde teclado. Devolverá dicho número. Se utilizará un módulo para calcular el factorial de un número. Recibirá dicho número y devolverá el factorial calculado. 60 5. MODULARIDAD DISEÑO: a) Parte declarativa: MÓDULOS LLAMADOS leerEntPos():entero factorial(entero):real VARIABLES n,m:entero result:real b) Representación algorítmica: combinatorio BLOCK n←leerEntPos() m←leerEntPos() while do m>n result←factorial(n) / (factorial(m)factorial(n-m)) escribir (result) BLOCK escribir ("Error") m←leerEntPos() CODIFICACIÓN: /***********************************/ /*** C O M B I N A T O R I O ***/ /***********************************/ #include <stdio.h> /* Prototipos de funciones invocadas desde el programa principal */ int leerEntPos(); double factorial(int); /* Programa principal */ int main() { int n,m; double result; } printf("Introduce un número: "); n=leerEntPos(); printf("Introduce otro número: "); m=leerEntPos(); while (m > n) { printf("Error, el segundo valor debe ser menor que %d: ",n); m=leerEntPos(); } result=factorial(n)/(factorial(m)*factorial(n-m)); printf("Combinaciones de %d elementos tomados de %d en %d (s.r.)= %.0f", n,m,m,result); return 0; /* leerEntPos */ /* Devuelve un número positivo leído desde teclado */ int leerEntPos() ... (aquí se situará la definición de la función leerEntPos) /* factorial(n) */ /* Devuelve el factorial de un número n */ ... (aquí se situará la definición de la función factorial) 61 5. MODULARIDAD 5.4 MÓDULOS QUE NO DEVUELVEN NINGÚN VALOR Como se indicaba en la introducción, el objetivo de la modularidad es dividir un problema en subproblemas más simples. Por ello, a veces puede convenir separar en un módulo un conjunto de instrucciones que representen una subtarea independiente dentro del programa, incluso aunque dicho módulo no proporcione ningún resultado al módulo llamador. Tal es el caso, por ejemplo, de un módulo que muestre por pantalla las instrucciones de uso de un programa. En la fase de diseño, la definición de un módulo que no devuelve ningún valor al módulo llamador se indicará omitiendo la parte :tipo del final de la cabecera del módulo. Además, en el diagrama de Tabourier, no se incluirá al final la operación devolver que se estudió en la sección anterior. En la fase de resolución en el ordenador, la cabecera de la función comenzará situando la palabra clave void en el lugar del tipo devuelto por la función y se omitirá la instrucción return al final del cuerpo de la función. La invocación de un módulo que no devuelve ningún valor se efectuará de forma idéntica a como se hace con otra función, es decir, identificador_función(lista de parámetros) teniendo en cuenta que ahora debe aparecer independientemente y no como parte de una expresión, ya que no hay un resultado asociado a la invocación. EJEMPLO 5.3. Desarrollar un módulo que muestre por pantalla la tabla de multiplicar de un número entero indicado como parámetro. ANÁLISIS: a) Datos de entrada: mult: valor del que se mostrará la tabla de multiplicar. Módulo llamador. b) Datos de salida: tabla[10]: Valores de la tabla de multiplicar de mult. Monitor M. llamador mult tablaMultiplicar c) Comentarios: Utilizaremos una variable para contabilizar el número de multiplicaciones mostradas durante la visualización de la tabla de multiplicar. DISEÑO: a) Parte declarativa: tablaMultiplicar(mult:entero) VARIABLES i:entero b) Representación algorítmica: tablaMultiplicar(mult) BLOCK escribir (“Tabla”,mult) i ← 1, 10, 1 for do escribir (i,mult,imult) 62 5. MODULARIDAD CODIFICACIÓN: /* tablaMultiplicar(mult) */ /* Calcula y muestra la tabla de multiplicar de 'mult' */ void tablaMultiplicar(int mult) { int i; printf(" TABLA DE MULTIPLICAR DEL %2d\n",mult); printf("=============================\n"); for (i=1; i<=10; i=i+1) printf("%10d X %2d = %2d\n",i,mult,i*mult); } 5.5 MÓDULOS QUE DEVUELVEN MÁS DE UN VALOR Las funciones devuelven, como máximo, un resultado asociado al nombre de la función. En los ejemplos anteriores, los parámetros se han utilizado para enviar valores desde el módulo llamador al módulo llamado. Este modo de transferir información entre módulos mediante parámetros se denomina pase de parámetros por valor y consiste en copiar el valor del parámetro real en el correspondiente parámetro formal (los parámetros real y formal representan direcciones de memoria distintas). Por lo tanto, el pase de un parámetro por valor constituye un canal de comunicación unidireccional desde el módulo llamador hacia el módulo llamado. A través de ellos solo puede enviarse información desde el módulo llamador hacia el módulo llamado ya que las modificaciones que pudieran producirse en los parámetros formales dentro del módulo llamado no quedan reflejadas en los parámetros reales especificados en la invocación. Así, aunque la función factorial del Ejemplo 5.1 se hubiera definido de forma que el parámetro formal n modificara su valor dentro de la función: double factorial(int n) { double f; if (n < 2) f=1; else { f=n; while (n > 2) { n=n-1; f=f*n; } } return f; } el resultado del cálculo combinatorio del Ejemplo 5.2 seguiría siendo correcto, ya que las modificaciones que sufre el parámetro formal n dentro de la función no afectan al parámetro real con el que se realiza cada llamada. Cuando necesitemos definir un módulo que devuelva más de un valor al módulo llamador, deberemos utilizar otro modo de transferir información entre módulos: el pase de parámetros por referencia. Este modo permite establecer un canal de comunicación bidireccional entre el módulo llamador y el módulo llamado. En concreto, consistirá en transferir al módulo llamado la dirección del parámetro real (utilizando el operador de dirección, &), en lugar de transferir una copia de su 63 5. MODULARIDAD valor (los parámetros real y formal representarán la misma dirección de memoria). Así, a diferencia de lo que ocurre con el pase de parámetros por valor, las modificaciones realizadas sobre un parámetro formal pasado por referencia también se efectúan sobre su correspondiente parámetro real, quedando así reflejadas en el módulo llamador. Un módulo puede pasar por referencia tantos parámetros como se quiera, ampliando así el número de valores que el módulo puede «devolver» al módulo llamador.1 Obsérvese que un parámetro pasado por referencia siempre actuará, al menos, como dato de salida del módulo (a veces, podrá actuar también como dato de entrada). En el caso de que el dato fuera sólo de entrada al módulo, el parámetro deberá pasarse por valor. EJEMPLO 5.4. Desarrollar un módulo que intercambie en el módulo llamador el valor de dos variables enteras. ANÁLISIS: a) Datos de entrada: x: valor a intercambiar. Módulo llamador. y: valor a intercambiar. Módulo llamador. M. llamador x,y b) Datos de salida: x': valor intercambiado (la y original). Módulo llamador. y': valor intercambiado (la x original). Módulo llamador. intercambio c) Comentarios: Será necesario utilizar una variable auxiliar que mantenga uno de los valores iniciales temporalmente, con el objetivo de que este no se pierda tras la primera asignación. DISEÑO: a) Parte declarativa: intercambio(*x:entero, *y:entero) VARIABLES aux:entero b) Representación algorítmica: intercambio(x,y) BLOCK aux←x x←y y←aux CODIFICACIÓN: /* intercambio(x,y) /* Intercambia los valores de x e y void intercambio(int *x, int *y) { int aux; } */ */ aux=*x; *x=*y; *y=aux; 1 Si bien el lenguaje C permite que un mismo módulo devuelva un valor asociado al nombre de la función y otros valores asociados a parámetros pasados por referencia, el uso simultáneo de ambos métodos no se considera una buena práctica de programación. Por ello, emplearemos el primer método cuando el módulo devuelva un único valor y el segundo cuando devuelva más de uno. 64 5. MODULARIDAD 5.6 ARRAYS COMO PARÁMETROS Como se estudió en el tema anterior, en lenguaje C, el identificador de un array es un puntero al primer elemento del array. Por ello, al pasar un array a un módulo como parámetro, realmente estaremos pasando la dirección del array y no su contenido. En otras palabras, cuando se trate de arrays, el pase de parámetros en C se hace siempre por referencia. Por lo tanto, habrá que prestar especial cuidado cuando se trabaje dentro de un módulo con parámetros de tipo array, pues las modificaciones que se hagan en el parámetro formal siempre se reflejarán en el correspondiente parámetro real del módulo llamador. En la declaración de parámetros formales de tipo array se indicará, entre corchetes, la longitud de cada dimensión, excepto la primera, que podrá omitirse (aunque no los corchetes). Por ejemplo, si queremos definir un módulo que lea desde teclado una matriz de enteros de 10×5 y la devuelva al módulo llamador, un posible prototipo de función sería: En algoritmia: En C: leerMatriz([][5]:entero) void leerMatriz(int [][5]); mientras que en la cabecera de la función sería: En algoritmia: En C: leerMatriz(m[][5]:entero) void leerMatriz(int m[][5]) 65 5. MODULARIDAD EJERCICIOS 1º) Un número complejo es de la forma a+bi, donde a y b son números reales e i= −1 . Desarrollar un programa dirigido por opciones de menú que lea dos números complejos (donde a+bi se introduce como un par de números reales a y b), permita al usuario seleccionar la ejecución de una operación (suma, resta, multiplicación o división) y muestre el resultado de la forma a+bi. Las cuatro operaciones se definen de la siguiente manera: (a+bi) + (c+di) = (a+c) + (b+d)i (a+bi) - (c+di) = (a-c) + (b-d)i (a+bi) · (c+di) = (ac-bd) + (ad+bc)i (a+bi) / (c+di) = [(ac+bd)/(c2+d2) ] + [(bc-ad)/(c2+d2)]i 2º) Calcular y mostrar por pantalla la edad de una persona a partir de la fecha actual y la de su nacimiento con formato dd mm aaaa, introducidas ambas por teclado. 3º) Escribir los n primeros números de la serie de Fibonacci a partir de un número natural n introducido por teclado. Nota: La serie de Fibonacci es 0,1,1,2,3,5,8,13,... de acuerdo con la ley siguiente: fibonacci(1) = 0 fibonacci(2) = 1 fibonacci(3) = 1 = fibonacci(2) + fibonacci(1) fibonacci(4) = 2 = fibonacci(3) + fibonacci(2) ... fibonacci(n) = fibonacci(n-1) + fibonacci(n-2) 4º) El desarrollo de la serie de Maclaurin para la función seno es sen x = x − x3 x5 x7 x9 − −... 3! 5! 7! 9! Evaluar y mostrar por pantalla el seno de x empleando n términos en el desarrollo, donde x y n se introducen por teclado (x vendrá expresado en radianes). 5º) Escribir un programa que lea números enteros de teclado hasta que el usuario acierte un número secreto generado aleatoriamente entre 1 y 100. Cada vez que se introduzca un número se debe indicar al usuario si es mayor o menor que el número secreto. Una vez acertado, se mostrará el número de intentos que se han empleado. Nota: La función interna random(x) genera un número aleatorio en el intervalo [0,x-1], donde el parámetro x y el valor devuelto son de tipo entero. Antes de utilizar esta función en el programa es necesario invocar a la función randomize() que inicializa el generador de números aleatorios. 6º) Para amortizar un préstamo de P euros en un banco, el cliente deberá devolver una cuota fija de C euros al mes hasta que haya completado la cantidad total prestada. Parte de la cuota mensual serán intereses, calculados como el I por ciento de la cantidad aún no pagada (capital pendiente). El resto del pago servirá para reducir dicho capital pendiente. Se desea realizar un programa para que, a partir de valores para P, C e I introducidos por 66 5. MODULARIDAD teclado, determine la siguiente información: a) b) c) d) e) f) Número de orden de cada cuota mensual. El importe aplicado cada mes a la reducción del capital pendiente. Intereses pagados cada mes. Capital pendiente al final de cada mes. Intereses acumulados al final de cada mes. La cuantía del último pago (ya que puede ser menor que C). Un ejemplo de ejecución para un préstamo de 6.000 euros, con una cuota mensual de 500 euros y un interés del 1% podría ser: Introduce la cantidad total del préstamo: 6000 Introduce el importe de la cuota mensual: 500 Introduce el interés aplicado: 1 MES Capital Intereses Pendiente T.Intereses =========================================================== 1 440.00 60.00 5560.00 60.00 2 444.40 55.60 5115.60 115.60 3 448.84 51.16 4666.76 166.76 4 453.33 46.67 4213.42 213.42 5 457.87 42.13 3755.56 255.56 6 462.44 37.56 3293.11 293.11 7 467.07 32.93 2826.04 326.04 8 471.74 28.26 2354.30 354.30 9 476.46 23.54 1877.85 377.85 10 481.22 18.78 1396.63 396.63 11 486.03 13.97 910.59 410.59 12 490.89 9.11 419.70 419.70 13 419.70 4.20 0.00 423.90 Cuota del último mes: 423.90 7º) La distancia del punto de caída de un proyectil lanzado con un ángulo a (en grados) y una velocidad inicial v (en metros por segundo), ignorando la resistencia del aire, viene dada por la fórmula v 2 × sen [ ×a/90 ] distancia = 9,81 Suponiendo que la diana se encuentra a 100 metros de distancia, simular un juego en el que el usuario introduce el ángulo y la velocidad de lanzamiento de un proyectil. Si el proyectil cae a menos de un 1 metro de la distancia a la diana se considera que ha dado en el blanco y el programa finaliza; en caso contrario, se le indica al usuario cuánto se ha alejado el proyectil del punto de lanzamiento y se le permite intentar un nuevo lanzamiento. El usuario seguirá lanzando hasta dar en el blanco, después de lo cual se mostrará el número de intentos empleados acompañado de uno de los mensajes EXCELENTE, BIEN, REGULAR, MAL o PÉSIMO, dependiendo de si el número de intentos es 1, es 2 o 3, está entre 4 y 6, es 7 u 8, o es mayor que 8, respectivamente. 8º) ¿Quién tiene menos?" es un juego de estrategia para dos jugadores en el que cada jugador piensa un número entre el 1 y el 5 y a continuación los comparan. Si coinciden o se diferencian en más de una unidad, cada jugador recibe un número de puntos igual al número que pensó. Si por el contrario, los números se diferencian en una unidad, el jugador que pensó el número menor recibe una cantidad de puntos igual a la suma de los dos números pensados. El juego consta de 10 rondas y después de cada una de ellas se acumulan los puntos. Gana el que alcance mayor número de puntos. Realizar un programa que permita jugar una partida entre el usuario y el ordenador, solicitando para cada ronda la jugada al usuario y actualizando a 67 5. MODULARIDAD continuación el marcador. Una vez finalizada la partida, se mostrará un mensaje indicando quién ha sido el vencedor. Un ejemplo de ejecución sería: RONDA 1: Introduce tu jugada ([1,5]): 3 Humano: 3, Máquina: 5. Difieren en Marcador: HUMANO 3 - MAQUINA 5 RONDA 2: Introduce tu jugada ([1,5]): 2 Humano: 2, Máquina: 2. Los números Marcador: HUMANO 5 - MAQUINA 7 ... RONDA 9: Introduce tu jugada ([1,5]): 3 Humano: 3, Máquina: 5. Difieren en Marcador: HUMANO 28 - MAQUINA 24 RONDA 10: Introduce tu jugada ([1,5]): 3 Humano: 3, Máquina: 4. Difieren en Marcador: HUMANO 35 - MAQUINA 24 Partida finalizada. ¡Enhorabuena!, has ganado. más de una unidad. coinciden. más de una unidad. una unidad. Nota: Las jugadas del ordenador se determinarán de forma aleatoria empleando la función interna random(x) explicada en el Ejercicio 5. 9º) Existen muchas series infinitas que convergen a π o a fracciones de π. Dos de estas series son la de Leibniz y la de Wallis. La serie de Leibniz viene dada por = 4 n 1 ∑ −1i1 2 i−1 i=1 y la de Wallis por = 4 donde n u ∏ vi i=1 i ui = 2 + 2 × (i div 2) vi = 1 + 2 × ((i+1) div 2) Mostrar por pantalla una tabla con las aproximaciones a π mediante las series de Leibniz y de Wallis desde 10 hasta n términos, donde n es un número introducido por el usuario entre 20 y 999, ambos inclusive. En cada línea deberá aparecer, además de las dos aproximaciones calculadas, el número de términos empleado, el error sobre el valor exacto de π y una indicación (L, W o =) de cuál de las aproximaciones es la mejor. 10º) Desarrollar un módulo que, recibiendo del módulo llamador un vector de enteros y su tamaño, devuelva si el vector está o no ordenado crecientemente. 11º) Desarrollar un módulo que ordene ascendentemente y devuelva un vector de enteros recibido del módulo llamador. 12º) “Las 4 en raya” es un juego de mesa para 2 jugadores que emplea un tablero en disposición vertical de f filas por c columnas. Los jugadores, alternativamente, van insertando fichas de su color (blancas o negras) en las columnas del tablero, que caen situándose unas encima de las otras. Gana el jugador que consigue formar una línea horizontal, vertical o diagonal de, al menos, 4 fichas de su color. Los valores de f y c deben ser especificados por los jugadores antes de comenzar cada partida y no deberán exceder de 20×20. 68 5. MODULARIDAD Desarrollar un módulo que procese y devuelva el efecto sobre el tablero de una jugada en cualquier momento de la partida. En concreto, recibiendo la información referente al estado del tablero antes de esa jugada (contenido y dimensiones) y la jugada a procesar (la columna donde se inserta la ficha y el color de la ficha), deberá devolver el nuevo contenido del tablero y el resultado de comprobar si la jugada fue válida o no (si la columna donde se intentaba insertar la ficha estaba o no llena). 13º) Desarrollar un módulo que inicialice y devuelva una matriz cuadrada de dimensiones n×n de la siguiente forma: la diagonal principal deberá contener un determinado valor entero v; las diagonales adyacentes a la diagonal principal, un valor superior en una unidad a v; las diagonales aún sin rellenar adyacentes a las dos anteriores, un valor dos unidades mayor que v, y así sucesivamente hasta rellenar completamente la matriz, resultando, por tanto, una matriz simétrica. El tamaño máximo de la matriz será 25×25. 14º) Desarrollar un módulo que, recibiendo una matriz cuadrada de valores enteros y su dimensión (10×10 como máximo), devuelva la suma de los valores situados por encima de la diagonal principal, la suma de los valores de la diagonal principal y la suma de los valores por debajo de la diagonal principal. 69 6 ESTRUCTURAS DE DATOS (II): REGISTROS En el Capítulo 4 conocimos el concepto de array, una de las estructuras de datos estáticas más habituales. Sin embargo, esta estructura tienen la limitación de permitir agrupar solamente datos de un mismo tipo. En este capítulo trataremos otra estructura de datos estática, los registros, que nos permitirán representar colecciones formadas por datos de distintos tipos. 6.1 REGISTROS Los arrays nos permiten agrupar datos con la condición de que sean todos de un mismo tipo. Existe un tipo de datos compuesto denominado registro que se puede utilizar para agrupar información relacionada entre sí pero formada por datos de tipos distintos. Un tipo registro está definido por un conjunto de componentes, denominados campos, que establecen su estructura. Cada dato de tipo registro podrá tener ciertos valores asignados a sus campos. Para trabajar con registros, es necesario en primer lugar definir la estructura de campos del tipo registro. Para ello, definiremos un nuevo tipo con el que posteriormente podremos declarar variables de dicho tipo. En la fase de diseño, la definición de tipos se situará en una sección específica de la parte declarativa, que titularemos TIPOS e irá situada inmediatamente después de la sección CONSTANTES. En concreto, la definición de un tipo registro utilizará el siguiente diagrama sintáctico: identificador de tipo registro = identificador de campo : tipo , 70 6. ESTRUCTURAS DE DATOS (II): REGISTROS En lenguaje C, el tipo registro se representa con el tipo estructura, cuya definición se sitúa inmediatamente después de la definición de constantes y obedece al siguiente diagrama sintáctico: typedef struct { identificador de campo tipo ; } , identificador de tipo registro ; Por ejemplo, para representar la siguiente información de un alumno: alumno id 5 nombre Antonio González grupo B nota 6.125 puede definirse un tipo registro de la siguiente forma: En algoritmia: En C: TIPOS tipoAlumno = id:entero nombre[35]:carácter grupo:carácter nota:real typedef struct { int id; char nombre[35]; char grupo; double nota; } tipoAlumno; Para utilizar un tipo registro, será necesario declarar variables de dicho tipo: En algoritmia: En C: VARIABLES alumno: tipoAlumno tipoAlumno alumno; Ahora bien, un registro contiene varios campos de información, por lo que para acceder a ellos de forma independiente será necesario especificar el campo que nos interesa. En concreto, deberá indicarse el identificador de la variable registro que lo contiene seguido de un punto (operador denominado descriptor de campo) y del identificador de campo (por ejemplo, alumno.id, alumno.nombre, alumno.grupo o alumno.nota). Un campo así especificado puede tratarse como si fuera una variable del mismo tipo que el campo y, por tanto, se le podrán aplicar las operaciones permitidas sobre variables de ese mismo tipo (por ejemplo, podrán realizarse operaciones aritméticas sobre alumno.nota). En C, el descriptor de campo es un operador con mayor prioridad que ningún otro (incluidos todos los operadores unarios estudiados hasta el momento). A diferencia de lo que ocurre con arrays, en C todos los valores de una variable registro pueden asignarse a otra variable registro en una sola instrucción mediante el operador de asignación: tipoAlumno alumno1,alumno2; (...) alumno2=alumno1; 71 6. ESTRUCTURAS DE DATOS (II): REGISTROS También, a diferencia de los arrays, en C un registro puede pasarse por valor así como devolverse asociado al nombre de la función. Por ejemplo, los siguientes prototipos de funciones son válidos: tipoAlumno leerAlumno(); void mostrarAlumno(tipoAlumno alumno); Por otro lado, al igual que en el tipo registro tipoAlumno uno de los campos era de tipo cadena, es posible que otros campos sean de cualquier otro tipo de datos compuesto. Por ejemplo, podrían representarse en el registro las calificaciones del alumno en 10 asignaturas distintas alumno id nombre grupo notas 0123456789 utilizando un campo de tipo vector para las calificaciones, del siguiente modo: typedef struct { int id; char nombre[35]; char grupo; double notas[10]; } tipoAlumno; (...) tipoAlumno alumno; Para referirnos a la calificación del alumno en la tercera asignatura utilizaremos la sintaxis alumno.notas[2]. Del mismo modo, a menudo es necesario representar en un programa, no un dato de tipo registro, sino una colección de datos de tipo registro. Esto se puede implementar utilizando arrays cuyos elementos son registros. Por ejemplo, lo más probable es que un programa requiera procesar la información de un conjunto de alumnos y no de un solo alumno. Así, la información de los alumnos matriculados en 3 titulaciones distintas, con 200 alumnos por titulación, puede representarse con la siguiente matriz de registros: 0 1 id nombre grupo notas id nombre grupo notas id nombre grupo notas 01234567890123456789 199 ... 0 ... 1 ... 2 0123456789 72 6. ESTRUCTURAS DE DATOS (II): REGISTROS En lenguaje C, la definición de esta estructura de datos sería: typedef struct { int id; char nombre[35]; char grupo; double notas[10]; } tipoAlumno; (...) tipoAlumno alumnos[3][200]; Para referirnos al grupo del vigésimo tercer alumno de la segunda titulación emplearemos la sintaxis alumnos[1][22].grupo, mientras que la sintaxis alumnos[0][33].notas[5] haría referencia a la sexta nota de trigésimo cuarto alumno de la primera titulación. EJEMPLO 6.1. Desarrollar un módulo que, recibiendo del módulo llamador un vector de registros de 'np' poblaciones, con el nombre de cada población y su censo (número de habitantes en unidades de millar) durante 'nd' décadas, muestre por pantalla, para cada población, su nombre y el número de orden de la década con mayor número de habitantes (si hay varias décadas con el mayor número de habitantes, deberá mostrar el número de orden de la primera de ellas). El número máximo de décadas es 10. M. llamador np, nd, lPob[np].nom, lPob[np].hab[nd] mayorCenso MÓDULO mayorCenso: 1º) ANÁLISIS: a) Datos de entrada: np: Número de poblaciones. Módulo llamador (np > 0) nd: Número de décadas. Módulo llamador (nd > 0) lPob[np].nom: Nombre de cada población. Módulo llamador. lPob[np].hab[nd]: Miles de habitantes de cada población en cada década. Módulo llamador. b) Datos de salida: lPob[np].nom: Monitor. decada[np]: Número de orden de la década con mayor censo de cada población. Monitor. c) Comentarios: Se supone que en algún módulo ascendiente se ha definido lo siguiente: CONSTANTES MAXDEC=10 TIPOS tipoPob= nom[40]:carácter hab[MAXDEC]:reales Se utilizará una variable índice para recorrer la lista de poblaciones y otra para recorrer los censos de cada población en busca del mayor censo. Se utilizará una variable para almacenar el número de orden de la década a la que pertenece el censo mayor de cada población durante cada búsqueda. 2º) DISEÑO: a) Parte declarativa: mayorCenso(lPob[]:tipoPob, np:entero, nd:entero) VARIABLES ip,id,iMax:entero 73 6. ESTRUCTURAS DE DATOS (II): REGISTROS b) Representación algorítmica: mayorCenso(lPob,np,nd) BLOCK for do ip ← 0, np-1, 1 iMax←0 id ← 1, nd-1, 1 lPob[ip].hab[id] > lPob[ip].hab[iMax] BLOCK for do escribir (lPob[ip].nom,iMax+1) if then iMax←id 3º) CODIFICACIÓN: #define MAXDEC 10 /* Número máximo de décadas */ typedef struct { char nom[40]; double hab[MAXDEC]; } tipoPob; (...) /* mayorCenso(listaPob,np,nd) */ /* Muestra, para cada población, su nombre y la década */ /* con mayor censo. */ void mayorCenso(tipoPob listaPob[], int np, int nd) { int ip,id,iMax; } for (ip=0; ip<=np-1; ip=ip+1) { iMax=0; for (id=1; id<=nd-1; id=id+1) if (listaPob[ip].hab[id] > listaPob[ip].hab[iMax]) iMax=id; printf("La población %s tiene su mayor censo en la década %d\n", listaPob[ip].nom,iMax+1); } (...) 74 6. ESTRUCTURAS DE DATOS (II): REGISTROS EJERCICIOS 1º) El juego “Cricket simplificado” es un juego de dardos en el que sólo se consideran los sectores de la diana del 15 al 20. Un sector se considera cerrado cuando el mismo jugador acierta en ese sector tres veces. Una vez que un jugador ha cerrado un sector, cada vez que vuelva a acertar en ese sector, se le sumará al resto de jugadores que no lo hayan cerrado una puntuación igual a su valor (por ejemplo, si un jugador ha cerrado el sector 15, cada vez que acierte sobre el 15 se le sumarán 15 puntos al resto de jugadores que no lo hayan cerrado). Desarrollar un módulo que procese y devuelva el efecto sobre el marcador de una tirada en cualquier momento de la partida. Para ello, recibiendo la información referente al estado del marcador antes de esa tirada (numero de jugadores y puntuación y estado de los sectores para cada jugador) y la tirada a procesar (jugador que ha lanzado y sector acertado), deberá devolver el nuevo contenido del marcador. Una tirada sólo alterará el marcador cuando se acierte sobre alguno de los números del 15 al 20. 2º) En el juego de los barcos dos jugadores sitúan barcos de distintas longitudes (2, 3 ó 4 casillas) en sendos tableros de 9×9 casillas. A continuación los jugadores realizan alternativamente jugadas, consistente cada una en indicar una casilla (fila y columna) del tablero oponente y cuyo resultado puede ser agua, tocado o hundido dependiendo de si la casilla no contiene ninguna porción de barco, contiene una porción de un barco que dispone de otras porciones aún sin destruir o contiene la última porción aún no destruida de un barco, respectivamente. Para desarrollar este juego con un programa informático se empleará una representación de tablero de tal forma que cada casilla represente la siguiente información (con el tipo de datos más adecuado en cada caso): indicador de si la casilla está o no ocupada por una porción de barco, tamaño del barco del que forma parte la casilla, dirección en la que está situado el barco (horizontal o vertical), posición que ocupa la porción respecto al barco completo (enumeradas las posiciones de izquierda a derecha para la dirección horizontal y de arriba a abajo para la vertical) e indicador de si la porción está destruida o no. Si la casilla no está ocupada, el resto de información se considerará indeterminada (podrá contener cualquier valor pues el programa ignorará dicha información). Desarrollar el módulo más adecuado que recibiendo del módulo llamador un tablero y una jugada, devuelva el tablero actualizado y el resultado de la jugada (agua, tocado o hundido). 3º) Se desea desarrollar un programa de cálculo de impuestos con las siguiente opciones de menú: 1. Añadir la información de cada unidad familiar 2. Listar la información de cada unidad familiar 3. Eliminar un miembro de una de las unidades familiares 4. Calcular y mostrar los impuestos de cada unidad familiar. 5. Salir del programa. La información de cada unidad familiar consistirá en el apellido (10 caracteres), nombre (10 caracteres) e ingresos de su miembros, donde se supondrá que dos personas consecutivas con el mismo apellido pertenecen a la misma unidad familiar. El impuesto se calcula como sigue: impuestos = porcentaje * ingresoAjustado 75 6. ESTRUCTURAS DE DATOS (II): REGISTROS donde { ingresoAjustado si ingresoAjustado 30.000 porcentaje = 120.000 0.25 en otro caso ingresoAjustado = ingresosTotales – (1200 * númeroDeducciones) y el númeroDeducciones es igual al número de personas de la unidad familiar sin ingresos. Cuando ingresoAjustado sea negativo, los impuestos serán 0 euros. El proceso de entrada de datos finalizará cuando se indique como apellido XXXX. El listado de impuestos consistirá en un listado en pantalla donde, en cada línea, deberá mostrarse el apellido de una familia y su impuesto calculado. Como máximo, podrán tratarse 100 personas en cada ejecución del programa. Por ejemplo, dada la entrada siguiente, Apellidos: Castilla Nombre: Rafael Ingresos: 10150 Apellidos: Castilla Nombre: María Ingresos: 13820 Apellidos: Castilla Nombre: Francisco Ingresos: 0 Apellidos: Pérez Nombre: Roberto Ingresos: 39350 Apellidos: Pérez Nombre: María Ingresos: 7210 Apellidos: XXXX el listado de impuestos sería: FAMILIA Castilla Pérez IMPUESTOS 4320.61 11640.00 4º) El sistema de mensajes cortos (SMS) permite enviar desde un teléfono móvil mensajes de texto con un máximo de 160 caracteres. Debido a su limitada extensión, es habitual el uso de abreviaturas que reducen el texto considerablemente. Desarrollar un programa que, empleando la tabla de sustituciones que se indica a continuación, abrevie un texto introducido desde teclado por el usuario y lo muestre en pantalla. TABLA DE SUSTITUCIONES UNO UN UNA DOS TRES CUATRO CINCO SEIS SIETE OCHO NUEVE BE 1 1 1 2 3 4 5 6 7 8 9 B VE CE DE EFE GE KA CA ELE EME ENE EÑE PE B C D F G K K L M N Ñ P CU ERE ESE TE UVE UBE ZETA CETA POR MAS MENOS Q R S T V B Z Z X + - 76