132
CAPÍTULO 4: FUNDAMENTOS DE PROGRAMACIÓN 4.1 INTRODUCCIÓN A LA PROGRAMACIÓN IMPERATIVA. Un computador es una máquina electrónica que puede resolver problemas ejecutando un conjunto de instrucciones que almacena internamente. Se llama programa a una secuencia de instrucciones que describe cómo ejecutar cierta tarea. Los circuitos electrónicos de un computador pueden reconocer y ejecutar directamente un conjunto limitado de instrucciones simples. Es lo que se conoce por lenguaje máquina o código máquina del computador (véase el capítulo 2). El lenguaje máquina raramente puede contener operaciones más complejas que: Realizar operaciones aritméticas con dos números: sumas, productos, comparaciones, etc. Mover datos de una parte de la memoria a otra. Saltar a una instrucción si se cumple una condición simple. Se intenta hacer el lenguaje máquina lo más simple posible, con el fin de reducir la complejidad y costo de la electrónica necesaria para interpretar cada una de las instrucciones que lo componen. Cualquier tarea que se desee hacer con el ordenador debe describirse con una secuencia de estas instrucciones simples. Por ejemplo, en la Figura 4.1 se ha escrito el equivalente en instrucciones de lenguaje máquina de una fórmula científica. Figura 4.1 En la Figura 4.1, las fórmulas que se han colocado tras cada punto y coma, son aclaraciones (comentarios) para entender mejor que está haciendo el programa. Cada instrucción realiza una de las operaciones simples antes indicadas, denotándose ésas con las tres primeras letras de la operación. Así, mul significa multiplicar; div implica dividir números, etc. A la derecha de cada operación se expresan los operandos que intervienen en ella: dos factores y un resultado para mul, dividendo, divisor y cociente resultante para div, etc. Las instrucciones mov son para mover o trasladar un dato desde la memoria principal a los registros de la CPU o viceversa. Entre corchetes [] se dan las direcciones de la memoria, expresadas aquí como símbolos que deberán tener un valor entero dentro del rango que admita la memoria. Además, para que este programa calcule el valor final de S (que quedará almacenado en cierta dirección [s] de la memoria), se habrán mov r1,[t] mul r2,r1,r1 ; t 2 mov r3,[a] mul r2,r2,r3 ; at 2 movi r4,#2.0 div r5,r2,r4 ; at 2 /2 mov r6,[vo] mul r6,r6,r1 ; V o t add r7,r5,r6 ; V o t+ at 2 /2 mov r8,[so] add r8,r8,r7 ; S o +V o t+ at 2 /2 mov [s],r8 2 0 0 2 1 at t V S S + + =

capitulo4

Embed Size (px)

Citation preview

Page 1: capitulo4

CAPÍTULO 4: FUNDAMENTOS DE PROGRAMACIÓN

4.1 INTRODUCCIÓN A LA PROGRAMACIÓN IMPERATIVA.

Un computador es una máquina electrónica que puede resolver problemas ejecutando un conjunto de instrucciones que almacena internamente. Se llama programa a una secuencia de instrucciones que describe cómo ejecutar cierta tarea. Los circuitos electrónicos de un computador pueden reconocer y ejecutar directamente un conjunto limitado de instrucciones simples. Es lo que se conoce por lenguaje máquina o código máquina del computador (véase el capítulo 2). El lenguaje máquina raramente puede contener operaciones más complejas que:

Realizar operaciones aritméticas con dos números: sumas, productos, comparaciones, etc. Mover datos de una parte de la memoria a otra. Saltar a una instrucción si se cumple una condición simple.

Se intenta hacer el lenguaje máquina lo más simple posible, con el fin de reducir la complejidad y costo de la electrónica necesaria para interpretar cada una de las instrucciones que lo componen. Cualquier tarea que se desee hacer con el ordenador debe describirse con una secuencia de estas instrucciones simples. Por ejemplo, en la Figura 4.1 se ha escrito el equivalente en instrucciones de lenguaje máquina de una fórmula científica.

Figura 4.1

En la Figura 4.1, las fórmulas que se han colocado tras cada punto y coma, son aclaraciones (comentarios) para entender mejor que está haciendo el programa. Cada instrucción realiza una de las operaciones simples antes indicadas, denotándose ésas con las tres primeras letras de la operación. Así, mul significa multiplicar; div implica dividir números, etc. A la derecha de cada operación se expresan los operandos que intervienen en ella: dos factores y un resultado para mul, dividendo, divisor y cociente resultante para div, etc. Las instrucciones mov son para mover o trasladar un dato desde la memoria principal a los registros de la CPU o viceversa. Entre corchetes [] se dan las direcciones de la memoria, expresadas aquí como símbolos que deberán tener un valor entero dentro del rango que admita la memoria. Además, para que este programa calcule el valor final de S (que quedará almacenado en cierta dirección [s] de la memoria), se habrán

mov r1,[t] mul r2,r1,r1 ; t2 mov r3,[a] mul r2,r2,r3 ; at2 movi r4,#2.0 div r5,r2,r4 ; at2/2 mov r6,[vo] mul r6,r6,r1 ; Vot add r7,r5,r6 ; Vot+ at2/2 mov r8,[so] add r8,r8,r7 ; So +Vot+ at2/2 mov [s],r8

200 2

1 attVSS +⋅+=

Page 2: capitulo4

108 ♦ Capítulo 4: Fundamentos de Programación.

tenido que almacenar antes los valores de los otros parámetros t, a, Vo, etc. en las posiciones de memoria adecuadas (en [t], [a], etc.).

Sin embargo, como habrá comprendido por este simple ejemplo, el lenguaje máquina es difícil de utilizar por parte de programadores humanos, debido, principalmente, a que:

Cada tipo de procesador suele tener su propio lenguaje máquina, por lo que un programa escrito en un lenguaje máquina no funciona en otro procesador que entienda otro distinto.

La mayoría de los lenguajes máquina son demasiado elementales: es difícil y tedioso utilizarlos.

Una forma de enfrentarse a este problema es crear un nuevo lenguaje, llamado lenguaje de programación de alto nivel, que sea más fácil de utilizar por un programador humano. El programador describe lo que debe hacer el computador escribiendo programas en lenguaje de alto nivel, que tiene un aspecto similar a los lenguajes que se utilizan para describir operaciones matemáticas. Por ejemplo, si tenemos una fórmula matemática como:

( )( )11 +−= xxy

puede calcularse, en un programa escrito en lenguaje de alto nivel, con un texto similar, algo como:

y = (x–1)*(x+1);

Esto es una “instrucción” en lenguaje de programación de alto nivel: dice cómo realizar una operación representándola de una manera simbólica, que puede ser leída por una persona. Para lenguajes de alto nivel, en lugar de instrucción se suele utilizar el término sentencia.

Además, los lenguajes de programación son formas de representación prácticamente independientes del computador que se va a usar. La sentencia o instrucción que hemos visto, podría funcionar en cualquier computador (independientemente del código máquina que tenga), siempre que se use el traductor adecuado, que transforme la sentencia en el código máquina de la CPU. Esto se debe a que, aunque y=(x–1)*(x+1); es muy fácil de entender para un humano, el computador es incapaz de interpretar directamente qué es lo que tiene que hacer, ya que sólo entiende operaciones en código máquina almacenadas en memoria. Es necesario, por tanto, traducir el programa en lenguaje de alto nivel a instrucciones equivalentes en código máquina. Existen, en general, dos aproximaciones a este proceso:

Compilación. El programa completo, normalmente almacenado en uno o varios ficheros de texto denominados código fuente, es procesado por un programa llamado compilador. El compilador genera un fichero ejecutable, que contiene el código máquina equivalente a todas las sentencias del programa fuente. Una vez compilado, el código fuente no se vuelve a usar, sino que se utiliza directamente el fichero ejecutable.

Interpretación. El código fuente es procesado por un programa llamado intérprete. El intérprete también traduce a código máquina, pero sentencia a sentencia. Para cada instrucción o sentencia del lenguaje de alto nivel, el intérprete la traduce a código máquina, ejecuta el código traducido y pasa a la siguiente sentencia. Cada vez que se quiera ejecutar el programa, necesitaremos tanto el código fuente como el programa intérprete.

Algunos lenguajes de programación pueden traducirse con ambos procesos, de forma que el programador puede elegir entre compilar su programa o ejecutarlo con un intérprete.

Sin embargo, hoy en día es mucho más frecuente compilar un programa que interpretarlo. Aunque la interpretación permite ejecutar directamente el programa (evitando el paso de compilación), la ejecución interpretada de un programa es necesariamente más lenta. Esto se debe a que, a la hora de ejecutar un programa interpretándolo, se debe en primer lugar consumir un tiempo en traducirlo, y después otro en ejecutarlo. Es decir, en la interpretación, la traducción y la ejecución van unidas en el tiempo. No obstante, si se opta por compilarlo, el tiempo de traducción se realizará independientemente de la ejecución. Así una vez compilado, disponemos de un fichero ejecutable, que se puede lanzar o ejecutar directamente sobre la CPU. Adicionalmente, la compilación añade una nueva ventaja: como la traducción se puede hacer antes de la ejecución, se puede optimizar todo lo que se pueda tal traducción, para que la ejecución posterior sea lo más rápida posible. Mientras que si se interpreta un programa, en la traducción no se puede invertir mucho tiempo, ya que eso demoraría también la ejecución.

Por otra parte, la interpretación posee también sus ventajas. La más evidente es que uno se ahorra el proceso de compilación, con la comodidad que supone, por tanto, escribir un programa y ejecutarlo

Page 3: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 109

directamente. Además, un programa escrito en lenguaje de alto nivel tiene un tamaño mucho menor (en general) que un ejecutable en código máquina, con el consiguiente ahorro de memoria de almacenamiento. Por último, hay que resaltar aquí, que la interpretación de programas permite portar o transportar más fácilmente un programa (escrito en lenguaje de alto nivel), de una máquina a otra que posea un código máquina diferente: sólo es necesario disponer del intérprete adecuado. Cuando se necesitan compilar los programas, la portabilidad (traslado) de un programa de una CPU a otra distinta, pasa obligatoriamente por el proceso de compilación previo, lo que supone a veces una complicación adicional (que puede ser elevada en algunos casos).

EJECUCIÓN (S.O.)

PROGRAMADOR

COMPILADOR

PROCESO DE COMPILACIÓN

ALGORITMO

LENGUAJE DE ALTO NIVEL (.C)

LIBRERIAS S.O. (I/O)

FICHERO EJECUTABLE (.EXE)

CPU

Figura 4.2

El proceso de compilación es tan habitual entre los programadores, que está sistematizado en una serie de pasos hasta llegar a la ejecución. En cada paso intervienen o se crean distintos ficheros. Todo esto se muestra en la Figura 4.2. Como se ve en ella, en primer lugar el programador escribe cierto algoritmo en un lenguaje de alto nivel, en nuestro caso, en lenguaje C, y por tanto escribe el programa en un fichero con extensión .c. Se suele denominar fichero fuente o de código. A continuación se ejecutaría el compilador para que se traduzca el código fuente a código máquina ejecutable. Aunque esto se suele organizar en dos pasos, de momento y durante los primeros apartados de este capítulo, los fundiremos en uno solo. De esa forma, el compilador, con ayuda de otros programas (almacenados en librerías) y del sistema operativo, genera un fichero ejecutable (extensión .EXE). Este fichero puede ser ejecutado directamente en la CPU para la cual fue generado, aunque se requiere casi siempre que en la máquina esté instalado el mismo sistema operativo y librerías que se usaron para la compilación.

4.1.1 Modelos abstractos de cómputo. Lenguajes imperativos.

Los lenguajes de programación permiten describir programas de forma simbólica. La descripción se hace basándose en determinados elementos básicos y formas de combinar tales elementos para construir programas tan complejos como sea necesario.

Existen multitud de lenguajes de programación distintos, que difieren a veces en aspectos generales y otras simplemente en detalles. Analizando estos lenguajes podremos observar que muchos de ellos utilizan elementos básicos y reglas de combinación similares, aunque difieren en detalles. Por ejemplo, la siguiente sentencia en lenguaje C

X= 28; Hace lo mismo que esta otra en lenguaje Pascal:

X:= 28; Ambas almacenan el valor 28 en una posición de memoria representada por el símbolo X.

Page 4: capitulo4

110 ♦ Capítulo 4: Fundamentos de Programación.

Si de un conjunto de lenguajes de programación extraemos los conceptos comunes, obtendremos un modelo abstracto de cómputo, que recoge los elementos básicos y reglas de combinación en forma abstracta, prescindiendo de la notación concreta de cada lenguaje de programación.

Existen diversos modelos abstractos de cómputo en los que se basan los lenguajes de programación actuales. El más extendido es el modelo de programación imperativa, ya que responde a la estructura interna habitual de un computador y es, históricamente, el primero en aparecer. Su nombre viene del hecho de que un programa aparece como una lista de órdenes, mandatos o sentencias a cumplir.

El orden o disposición de ejecución de las sentencias de un programa imperativo es en principio, aquel en que están escritas las mismas. Esto es, en primer lugar se ejecuta la primera línea de código escrita por el programador, luego la segunda, y así hasta la última sentencia del programa. Sin embargo este orden o flujo de ejecución puede alterarse en caso necesario. Con ello se consigue no ejecutar un trozo de programa o repetir determinadas partes del mismo. Para ello, los lenguajes imperativos suelen contener las llamadas sentencias de control de flujo, que simplemente modifican el flujo de ejecución, “saltando” de una sentencia a otra que no es la siguiente del programa. Para identificar la sentencia destino del salto, algunos lenguajes numeran cada línea de programa (caso del BASIC). Otros, como el C y el Pascal, añaden etiquetas de salto en aquellas líneas de programa que sean susceptibles de ser destinos de una instrucción de salto.

4.1.2 El lenguaje de programación C.

El lenguaje de programación C es uno de los lenguajes imperativos más extendidos en la actualidad. Fue creado entre 1970 y 1972 por Brian Kernighan y Dennis Ritchie, los cuales escribieron un libro1 que define con exactitud este lenguaje. En principio, el lenguaje C se creó para escribir el código del sistema operativo UNIX2, pero su uso se extendió rápidamente en computadores basados en este sistema operativo, y pronto gran parte de las aplicaciones UNIX se escribieron en este lenguaje. Así, aunque originalmente se utilizó para escribir software de sistemas operativos, actualmente se usa para todo tipo de aplicaciones, en una gran variedad de computadores y con sistemas operativos muy distintos: desde grandes mainframes a pequeños microcontroladores.

A mediados de los ochenta, la definición del lenguaje C se convierte en un estándar internacional ISO. Las definiciones y ejemplos que veremos aquí se basan en este estándar, denominado ANSI C.

4.2 COMPOSICIÓN SECUENCIAL. LA ASIGNACIÓN.

Como la mejor forma de conocer el funcionamiento del lenguaje C es utilizándolo, comenzaremos con un ejemplo muy sencillo, que muestra por pantalla el siguiente texto:

Hola, soy un programa C bastante simple, pero educado.

Un programa C que hace esto es:

#include <stdio.h> main() { printf("Hola,"); printf(" soy un programa C bastante simple, pero educado.\n"); }

Aunque no hace ningún trabajo útil, su simplicidad nos sirve para introducir la estructura general de un programa C.

1 El lenguaje de programación C. Ed. Prentice Hall. ISBN: 9688802050 2 Recuerde que un Sistema Operativo no es más que un programa destinado a organizar el acceso al hardware de una

máquina, con el fin de que el resto de las aplicaciones vean los recursos hardware con una estructura lógica.

Page 5: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 111

4.2.1 La función main.

Un programa C suele estar dividido en funciones (que se llaman procedimientos o rutinas en otros lenguajes). Cada función ejecuta una parte de un programa, pudiendo recibir unos valores (de entrada) y pudiendo devolver otros (de salida). El programador puede hacer cuantas funciones necesite y darles los nombres que quiera, pero siempre debe haber una función main (en inglés significa “principal”), que es la que se ejecutará en primer lugar. Por ello, main() es conocida como función principal. La primera línea de nuestro programa (la que empieza por #include) la olvidaremos de momento, y la explicaremos un poco más adelante.

Trataremos con más detalle el uso de funciones en la sección 4.5. En los ejemplos que veremos hasta entonces usaremos una sola función: main().

Los paréntesis tras el nombre main indican que es un nombre de función. Todo lo que hay entre las llaves { ... } que siguen al nombre es el cuerpo de la función. El cuerpo esta formado por una lista de sentencias, separadas por el carácter ‘;’. En principio, las sentencias se procesan de forma secuencial, esto es, se comienza por la primera sentencia de la función main(), y después de procesar una sentencia se continúa con la siguiente. En las secciones 4.3 y 4.4 veremos cómo se modifica este comportamiento secuencial, explicando las sentencias de control de flujo.

Nuestro ejemplo tiene sólo dos sentencias. Al ejecutar la primera:

printf("Hola,");

el computador muestra en pantalla el texto comprendido entre las comillas, dejando el cursor en la misma línea. A continuación el computador ejecuta la siguiente:

printf(" soy un programa C bastante simple, pero educado.\n");

de forma similar, pero ahora deja el cursor en la siguiente línea de la pantalla. Hace esto porque el texto dentro de las comillas contiene un código especial, \n, que le indica al ordenador que debe pasar al inicio de la siguiente línea de la pantalla.

Como no hay ninguna otra sentencia antes del carácter ‘}’, el programa finaliza su ejecución terminada la segunda sentencia.

Sintaxis del lenguaje C.

La sintaxis de un lenguaje de programación es el conjunto de reglas que debemos seguir para que el compilador sea capaz de reconocer nuestro programa como un programa C válido. Por ejemplo, un programa debe tener el siguiente formato general:

main () { Lista_de_sentencias }

Esta regla nos dice que para que el compilador reconozca nuestro programa debemos escribir al menos todas las palabras y signos que no están en letra cursiva. Las partes en cursiva se reconocen usando otras reglas sintácticas. Estas reglas definen o dicen cómo debe ser una lista de sentencias. Tales reglas nos dicen, por ejemplo, que la lista_de_sentencias puede estar vacía, por lo que

main() { }

es un programa C perfectamente válido, que el compilador reconocerá sin problemas (aunque no hará nada útil).

Page 6: capitulo4

112 ♦ Capítulo 4: Fundamentos de Programación.

Una característica de la sintaxis del C es que los espacios en blanco y retornos de carro que aparecen en las reglas sintácticas son opcionales. Esto quiere decir que nuestro programa de ejemplo se puede escribir en una sola línea:

main(){printf("Hola",); printf(" soy ... educado.\n");}

Su sintaxis es perfectamente válida y funciona exactamente igual, aunque es más difícil de leer para los humanos (no para el computador).

Por tanto, aunque el lenguaje no obliga a ello, se suele colocar una sentencia por línea y los espacios de forma que el programa se pueda leer cómodamente.

Funciones estándar de C.

printf() es lo que se denomina una función estándar de C, es decir, una función que está incluida con el compilador. Para poder usarla dentro de un programa hay que decir en el mismo cómo es su forma: eso está en la línea #include <stdio.h>. De momento dejaremos esto así y lo explicaremos en la sección 4.2.2.

Para poder trabajar o llamar a una función estándar hay que conocer qué necesita y qué hace. De la función printf hay que saber:

Su nombre: cada vez que se coloca printf en un programa le estamos diciendo al compilador de C que queremos usar esta función.

Lo que se quiere imprimir, que irá entre los paréntesis. En general, la expresión (números o texto) que va entre los paréntesis de una función se llaman parámetros de la función. Por ejemplo, si en matemáticas se quiere calcular el valor de log(3x2), el parámetro de la función logaritmo sería 3x2. En lenguaje C, algunas funciones necesitan que le demos datos para poder hacer su trabajo y otras no. Por ejemplo, printf necesita el texto que hay que mostrar por pantalla (el cual se entrecomilla), pero para la función principal main no es obligatorio.

Qué hace y cómo funciona: printf() escribe texto en la pantalla del computador. Los detalles de funcionamiento aparecen en el manual del compilador que se esté usando. El manual nos dice, por ejemplo, que si colocamos el código \n dentro del parámetro de printf(), se imprimirá un salto de línea.

Un compilador suele tener decenas de funciones estándar, aunque su número exacto y los detalles concretos de funcionamiento de las funciones estándar de C dependen de cada compilador.

4.2.2 Variables y constantes.

Las variables nos permiten almacenar datos. Estos datos pueden ser de distinto tipo o formato numérico y de distinto tamaño (en bits o bytes). Por ejemplo, un dato numérico puede almacenarse en una variable de tipo entero o en otra de coma flotante simple precisión. Recíprocamente, 32 bits (4 bytes) de la memoria pueden servir para contener un número de coma flotante simple precisión, o para 4 letras según el convenio ASCII. Es decir, que las variables fijan una interpretación de los bits de memoria o datos de que disponemos en el programa.

Las variables pueden ser manipuladas usando expresiones de distinto tipo. Una expresión es un conjunto de operaciones que realizan cálculos con datos o con los datos contenidos en las variables. Por ejemplo, las expresiones aritméticas son las más comunes.

Para introducir el uso de variables en C vamos a ver el siguiente programa, que nos permite convertir pesetas en euros:

Page 7: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 113

main() /* Este programa pasa de pesetas a euros */ { int Pesetas; /* Define una variable entera */ float SonEnEuros; /* Define una variable real */ /* Imprime un mensaje al usuario diciendole lo

que tiene que hacer */ printf("Por favor, dime cuantas pesetas tienes: "); scanf("%d", &Pesetas); /* Aqui el programa se detiene hasta

que el usuario teclea un numero entero y pulsa la tecla de retorno de carro (↵) */

SonEnEuros = Pesetas/166.386; // Convierte a euros // Muestra por pantalla el resultado printf("Entonces tienes %f Euros\n",SonEnEuros ); }

Figura 4.3

Comentarios. Antes de comenzar analizando el programa, hay que señalar que gran parte del texto que contiene no es

procesado por el compilador de C: son los comentarios. El compilador ignora cualquier cosa que se encuentre entre los caracteres /* y */. También es muy frecuente usar dos barras // para indicar el comienzo de un comentario, acabando éste al final de la línea de código (como en // Convierte a euros).

Los comentarios son útiles para hacer el programa más comprensible a las personas que lo lean (incluyendo al propio programador). Es muy importante comentar adecuadamente la utilidad de cada variable y de cada sentencia de un programa, tanto, que incluso se han definido normas para comentar los programas correctamente. En lo que sigue le aconsejamos que use habitualmente comentarios en todos sus programas.

Declaración de variables.

Las variables son elementos del programa que nos permite manejar datos. En nuestro ejemplo, las usaremos para almacenar el dato que nos da el usuario y hacer los cálculos de conversión. Pero antes de poder usarlas tenemos que declararlas: decir cómo se llaman y qué tipo de datos contienen. De esto se encargan las sentencias de declaración de variables, que se deben colocar al principio del cuerpo de la función. La sentencia:

int Pesetas;

Indica al computador que debe reservar espacio en memoria para una variable llamada Pesetas. La palabra int (del inglés “integer”, entero) indica el tipo de datos que contendrá: números enteros.

Los nombres de las variables sirven para que podamos usar los datos que contienen. Cada vez que en el programa aparezca Pesetas, por ejemplo, nos estaremos refiriendo al dato que hay en esta variable.

Para formar el nombre de una variable hay que respetar algunas reglas sencillas:

Puede ser una combinación de letras, números y el carácter ‘_’. No use letras o símbolos existentes en español y no en inglés (como la eñe o los acentos). La longitud máxima del nombre depende del compilador (los compiladores compatibles con ANSI C permiten nombres de hasta 31 caracteres).

Para el lenguaje C las letras mayúsculas y minúsculas son diferentes: por ejemplo, los nombres Pesetas, pesetas, PESETAS y peSetas son completamente distintos para el compilador.

No se pueden usar palabras reservadas: palabras que el lenguaje C usa para algún otro cometido. No podemos declarar ninguna variable llamada int, float, long o for, por ejemplo.

Y por supuesto, para hacer más comprensible el programa, es más que aconsejable que el nombre de una variable sirva como descripción de su cometido y esté relacionado con su uso. Así el programa de la Figura 4.3

Page 8: capitulo4

114 ♦ Capítulo 4: Fundamentos de Programación.

quedaría menos claro si las variables se hubieran bautizado con el nombre de p y s, en lugar de Pesetas y SonEnEuros.

La sintaxis de una declaración es:

especificador Lista_de_nombres ;

Lista_de_nombres es una lista de uno o más nombres de variable separados por comas, y especificador indica el tipo de datos que se almacenan en cada una de esas variables.

Tipos de variables.

La Tabla 4.1 muestra los llamados tipos de datos simples de C, que son aquéllos que contienen números enteros o reales. Los distintos tipos de enteros se diferencian en su tamaño (la cantidad de memoria que usan para almacenar el dato) y en si contemplan o no signo. Los distintos tipos de reales sólo se diferencian en su tamaño.

Es importante señalar que en lenguaje C no define exactamente el tamaño de los tipos de datos simples, sino que depende del compilador que se esté usando. Es decir, este lenguaje no dice que un int tiene que ser de 32 bits, por lo que algunos compiladores usan enteros de 32 bits y otros de 16. Los tamaños especificados en la tabla corresponden a la mayoría de compiladores modernos para el sistema operativo Linux y para las versiones de Windows a partir de Windows 95 (entre ellos Builder C++ v5.0 y GNU C para Linux). Los compiladores antiguos de C para el sistema operativo MS-DOS o Windows 3.1 usaban int de un tamaño de 16 bits, por ejemplo.

Tipo Variable que define

char Número entero con signo de 8 bits, que suele interpretarse como carácter ASCII. Rango entre –128 (–27) y 127 (27-1).

int Número entero con signo de 32 bits. Puede almacenar números entre −2,147,483,648 (−231) y 2,147,483,647 (231−1)

float Números reales de simple precisión.

double Números reales de doble precisión

Tabla 4.1

Estas son algunas declaraciones válidas de variables:

int Numero1, Numero2; int Resultado; float Temperatura, Presion, Humedad; /* numeros reales */ char TeclaPulsada; /* caracter */ int x=10, y=15, z=0, n; /* a los enteros x, y, z se les da

un valor inicial */ int copia_de_x = x;

El contenido de inicial de una variable al comenzar el programa no está definido, a menos que se le asigne o se inicialice con un valor inicial explícitamente en el momento de la declaración. Tal es el caso de las variables inicializadas, como x, y, z, copia_de_x.

Los datos almacenados en variables pueden cambiarse durante la ejecución del programa. Por ejemplo, en la sentencia siguiente se llama a una función estándar de C:

scanf("%d", &Pesetas);

la cual pedirá al usuario que teclee un número entero, cuyo valor se introducirá en la variable Pesetas. El funcionamiento de scanf() se explicará al final de esta sección.

Por otro lado, la sentencia:

Page 9: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 115

SonEnEuros= Pesetas/166.386; /* Convierte a euros */

almacena en la variable SonEuros el resultado de la conversión.

Modificadores de los tipos de variables.

El tipo de una variables se puede modificar ligeramente en su declaración utilizando otras palabras claves, denominadas modificadores de tipo. En concreto se puede alterar el tamaño que ocupa en memoria un número entero y si éste se almacenará con o sin signo.

El modificador unsigned (del inglés “sin signo”) aplicado delante de la declaración de una variable entera o carácter, implica que tal variable se almacenará siempre con signo positivo. Si no se pone ningún modificador, la variable será “signed”, o sea con signo en notación complemento a 2 (por ello, el modificador signed no suele usarse). En la Tabla 4.2 se muestra el rango de las dos variables que se pueden declarar sin signo. Compárese con el rango de la Tabla 4.1

También hay que aclarar que no tiene sentido aplicar unsigned sobre una variable real, puesto que los formatos estándar IEEE754 para coma flotante no definen ningún tipo de real sin signo.

Tipo Variable que define unsigned char

Número entero entre 0 y 255 (8 bits) o carácter ASCII unsigned int

Número entero entre 0 y 4,294,967,295(232−1) (32 bits)

Tabla 4.2

Existen otros modificadores que varían el tamaño de las variables. En concreto, si añadimos short a una variable entera, ésta sería un entero corto (“short” significa corto). Si se usa long (en inglés largo), entonces tenemos un entero largo. El problema es que, como pasa con el tamaño de las variables int, la alteración que producen estos modificadores depende del compilador. En algunos el entero largo es de 64 bits y en otros de 32 bits. Idem para los enteros cortos: puede ser de 16 bits o de 8 bits. Con todo esto puede incluso que un modificador no altere el tamaño si así lo tiene definido el compilador. Para la mayoría de compiladores modernos sobre sistema operativo Linux y para versiones de Windows a partir de Windows 95 (entre ellos Builder C++ v5.0 y GNU C para Linux), los tamaños son los que se dan en la Tabla 4.3.

Tipo Variable que define short int

Número entero de 16 bits entre –32768 (–215) y 32767 (215–1). long int

Número entero de 32 bits entre −2,147,483,648 (−231) y 2,147,483,647 (231−1).

Tabla 4.3

También se permiten las combinaciones de modificadores que tengan sentido, como unsigned long o signed short. La problemática y discrepancia del tamaño de las variables, se resuelve en C gracias a una palabra clave o reservada que aplicada sobre una variable o sobre un tipo devuelve el tamaño en bytes que el compilador le adjudica. Este operador se llama sizeof (en inglés “tamaño de”), y se usa simplemente así:

tamano_long_int = sizeof (long int); /* Introduce en la variable tamano_long_int el tamanyo de una de estas variables */

De esa forma el valor que devuelve la expresión sizeof (char) o sizeof (unsigned char)

será 1 en cualquier compilador, sizeof (float) devuelve siempre 4, y sizeof (double) devolverá siempre 8. Evidentemente para una variable entera la cantidad de números que pueden representarse con ella, es 2 elevado al tamaño en bits de la variable.

Page 10: capitulo4

116 ♦ Capítulo 4: Fundamentos de Programación.

También el modificador long aplicado sobre una variable double, modifica su longitud y la convierte en número en coma flotante extendido, cuyas características recordemos que son las que se muestran en la Tabla 4.4.

Tipo Variable que define long double Número real de 80 bits de tamaño entre -3.362103143112×10-4932 y

1.189731495357×104932

Tabla 4.4

En resumen, en la Tabla 4.5 se muestra cuándo tiene o no sentido anticipar cada uno de los modificadores a cada tipo de variable simple.

char int float double

short NO SI NO NO

long NO SI NO SI

unsigned SI SI NO NO

Tabla 4.5

Constantes.

Otros datos, sin embargo, no cambian durante la ejecución: son las constantes. Tal es el caso, por ejemplo, del número de pesetas por euro: 166.386. Al igual que las variables, las constantes pueden ser de distinto tipo, y se les puede asignar un nombre, aunque la forma de hacerlo es diferente.

La siguiente línea da el nombre PTS_POR_EURO a la constante 166.386:

#define PTS_POR_EURO 166.386

En principio, esta definición se puede colocar en cualquier del programa, siempre que esté antes de la sentencia donde se usa PTS_POR_EURO:

SonEnEuros= Pesetas/PTS_POR_EURO;

Sin embargo, se suelen colocar al principio del programa, antes de comenzar la función main, de forma que todas las definiciones de constantes estén localizadas en el mismo sitio. Hay que aclarar que el símbolo almohadilla ‘#’ es especial en lenguaje C, y sirve para hacer cambios antes de compilar dentro el propio fichero de código que ha escrito el programador. Así, por ejemplo la línea que empieza por #define, produciría una sustitución de PTS_POR_EURO por 166.386, dentro del fichero de extensión C donde escribimos nuestro programa. A continuación el código se compilaría. De manera análoga, la primera línea de todos nuestros programas hasta ahora también empezaba por tal símbolo: #include <stdio.h>. Esta línea hace que a la hora de compilar nuestro programa se incluya (en inglés “include” quiere decir incluir) todo el fichero llamado stdio.h, el cual contiene la descripción de las funciones estándar de C de entrada/salida. En concreto, el nombre de tal fichero es la abreviatura de la expresión standar input/output, y su extensión .h, se debe a que es un fichero de cabecera (“header”), es decir de los que deben ponerse antes del main(), para describir las funciones estándar que se van a usar. A lo largo de este texto iremos viendo algunos otros ficheros de cabecera, para usar otras funciones estándar de C. Por todo lo anterior, el procesado de las líneas que empiezan por el símbolo almohadilla ‘#’ no se llama compilación (ya que no se está traduciendo nada), sino preprocesamiento del código.

Por otro lado, la línea #define ... no dice explícitamente de qué tipo es una constante, aunque el compilador es capaz de conocerlo por su aspecto. En la Tabla 4.6 se pueden ver algunos ejemplos.

Page 11: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 117

Constantes Tipo asumido 0, 125, -12

22333

0x10, 0xA000, 0xa000

int o char int

int definidos como números hexadecimales: 0x10=16, 0xA000= 40,960

'0', 'A', '-'

'\x21', '\xA2'

'\n', '\t'

char

char definidos con el código ASCII del carácter ('\x21'= 'A') char definidos como secuencias de escape3 (retorno de carro y

tabulador) 0.0, 100.0, 3.14, .3333

1.0e3, .84e-7, 54.1E-2

float definidos usando notación decimal. float definidos usando notación exponencial (los equivalentes en

notación científica son 1.0×103, 0.84×10-7 y 54.1×10-2)

Tabla 4.6

Los números enteros suelen escribir se usando dígitos en base diez, sin signo o precedidos por los signos + ó –. Si empieza por 0x estamos ante un entero expresado con dígitos hexadecimales.

Los caracteres se definen siempre usando las comillas simples ' '. En el interior de las comillas colocamos directamente el carácter que queremos escribir, por ejemplo, los caracteres alfabéticos, numéricos y muchos signos de puntuación. Algunos códigos ASCII no se pueden teclear directamente, por lo que hay que usar un formato especial llamado secuencia de escape. Las secuencias de escape más utilizadas y los caracteres que representan se muestran en la Tabla 4.7.

Secuencia Carácter que representa \a Carácter de alarma \b Retroceso \f Avance de hoja \n Nueva línea \t Tabulador \\ Barra invertida \' Comilla simple \" Comilla doble \0oo Código ASCII en octal, (oo=dígitos octales) \xhh Código ASCII en hexadecimal, (hh=dígitos hexadecimales) \? Signo de interrogación \r Vuelta al principio de una línea (retorno de carro)

Tabla 4.7

Las constantes reales se pueden escribir usando notación decimal colocando un punto entre la parte entera y la parte decimal del número. Se puede usar notación exponencial, que es equivalente a la notación científica. Primero se coloca la mantisa, seguido del carácter e ó E y por último un entero (puede llevar signo negativo) que indica el exponente:

3 Las llamadas secuencias de escape son símbolos especiales, que al imprimirlos sobre una pantalla de texto, en lugar de

imprimir un carácter realizan una acción especial, como mover el cursor hacia abajo, cambiar el tipo de letra que se imprimirá desde entonces, insertar el espacio de un tabulador, etc.

Page 12: capitulo4

118 ♦ Capítulo 4: Fundamentos de Programación.

298.001×1030 equivale a 298.001e30, o a 298.001e+30, o a 298.001E+30

Más sobre printf.

Algunos de los printf() que usamos en el ejemplo de la Figura 4.3 son ligeramente distintos de los que hemos visto hasta ahora, como:

printf("Entonces tienes %f Euros\n",SonEnEuros );

Éste tiene dos parámetros: el texto a imprimir y un nombre de variable. El texto a imprimir contiene, además, el código %f. Con esto le estamos diciendo que en esa posición tiene que insertar un valor de tipo float. El valor lo coge de la lista de parámetros que hay tras el primero: el número que contenga SonEnEuros en este caso.

Al primer parámetro de una función printf() se le llama texto de formato, porque contiene el mensaje a imprimir y cuántas variables va a usar, de qué tipo y el formato en que se imprimen. Para ello se usan, principalmente, los códigos de la Tabla 4.8.

Código Formato de Impresión %d Imprime un entero usando dígitos decimales. Usará un ancho de campo de impresión tan grande

como sea necesario. Para imprimir 523, por ejemplo, usará 3 caracteres. Se le puede indicar que use un ancho mínimo: con %5d, por ejemplo, 523 se imprimiría en un campo de 5 caracteres de ancho (rellena con espacios en blanco a la izquierda).

%c Imprime un carácter ASCII %f Imprime un real de tipo float. %5f imprimirá la parte entera en un campo mínimo de 5 caracteres,

mientras que %5.3f imprime la parte entera en 5 caracteres y la decimal en 3. %x Imprime un entero con dígitos hexadecimales.

Tabla 4.8

El siguiente ejemplo imprime valores de distinto tipo:

printf("%d es entero. \n%c es caracter. \n%f es real. \n", Pesetas, 'E', 166.386);

Hay que señalar que la lista de valores o parámetros que hay tras el texto de formato siempre tiene que ser igual al número de códigos con % que se especifican dentro del texto. De lo contrario se imprimirán en pantalla valores absurdos.

La función scanf.

La función scanf() es una función estándar de C que permite leer datos desde la consola del ordenador. La sentencia:

scanf("%d", &Pesetas);

detiene la ejecución del programa hasta que el usuario teclee un número entero en la consola del ordenador. Al igual que printf(), tiene un texto de formato que indica el tipo de las variables que va a leer utilizando los mismos códigos de campo:

scanf("%f %d %c", &Real, &Entero, &Caracter);

Después del texto de formato, sigue una lista con tantos nombres de variables como códigos con % contenga el texto. Además, esta función necesita que cada nombre de variable vaya precedido por el operador & (se explicará el por qué de esto en el apartado 4.7 de este texto).

Page 13: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 119

4.2.3 La sentencia de asignación.

La sentencia de asignación es la forma más sencilla de almacenar un dato en una variable. Volviendo al ejemplo de la Figura 4.3:

SonEnEuros= Pesetas/166.386; /* Convierte a euros */

evalúa la expresión aritmética de la derecha de ‘=’ y almacena el resultado en SonEnEuros. El formato general de una sentencia de asignación es:

Nombre_de_variable = Expresion ;

donde:

Nombre_de_variable es el nombre de una variable definida previamente en alguna sentencia de declaración.

‘=’ es el operador de asignación. Indica que el valor que se calcule en Expresion debe ser almacenado en Nombre_de_variable.

Expresion indica cómo debe ser calculado el valor a almacenar. No debe confundirse este operador de asignación ‘=’ con el símbolo de igualdad de una ecuación

matemática. En una asignación la operación realizada consiste en introducir el valor de la derecha en la izquierda (variable), aunque esto lleve a que la igualdad se cumpla finalmente después de ejecutar la sentencia. Pero hay que dejar claro aquí que no es lo mismo asignación que ecuación; ya que en lenguaje C tiene sentido:

x = 5 ;

pero no:

5 = x ; /* daria un error de compilacion */

Igualmente en C podemos escribir:

x = 2 * x ;

Esta sentencia se ejecutará introduciendo en la variable x el doble de lo que contenía antes (mientras que si lo anterior fuera una ecuación matemática, entonces la solución única sería x = 0).

Desgraciadamente en C se usa este mismo símbolo para la asignación (en otros lenguajes como Pascal la asignación se realiza con dos símbolos: ‘:=’, quedando patente esta diferencia). En lenguajes algorítmicos, en general se suele usar una flecha hacia la izquierda para indicar una asignación:

x ← 5 ; x ← 2 * x ;

También puede haber operaciones de asignación dentro de una sentencia de declaración de variables:

int PosicionInicial = 0, PosicionFinal = 10000;

indica que el computador hace la asignación antes de comenzar a ejecutar el código. El formato general es idéntico al de la sentencia de asignación, con la diferencia de que no termina con ’;’ (los caracteres ’,’ y ’;’ del ejemplo anterior no forman parte de la asignación, sino de la declaración de variables).

4.2.4 Expresiones aritméticas.

Una expresión es un conjunto de operaciones que dan como resultado un valor. El tipo de operaciones que el lenguaje C permite depende de los datos con que se opera y del tipo de resultado. En este apartado veremos las expresiones más comunes: las aritméticas, las cuales dan como resultado un número.

Page 14: capitulo4

120 ♦ Capítulo 4: Fundamentos de Programación.

Una expresión puede ser simplemente una constante o el valor de una variable:

X = 100; /* la expresion es el valor 100 */ PrecioActual = PrecioInicial; /* La expresion es el valor

de PrecioInicial */

o puede ser una operación aritmética entre dos expresiones más pequeñas. C permite suma, resta, multiplicación y división:

Dos = 3 - 1; /* resta entre dos constantes (expresiones) */ Cuatro = Dos + Dos; /* Suma del valor de dos variables */ PVP = PrecioSinIVA*1.16; /* Multiplica el valor de una

Variable y una constante */ Media = Total/Num_elementos; /* Divide el valor de una

variable por el de otra */

O puede ser una operación de cambio de signo de una expresión más pequeña:

X = - YMax; /* Cambia el signo al valor de una variable */

En general, una expresión puede tener una de las formas de la Tabla 4.9.

Expresión Resultado de la expresión Constante El resultado de la expresión es el valor de la constante

Nombre_de_Variable El resultado de la expresión es el valor de la variable

Expresion1 + Expresion2 Suma Expresion1 y Expresion2, que son, a su vez, expresiones más pequeñas

Expresion1 – Expresion2 Resta Expresion2 a Expresion1 Expresion1 * Expresion2 Multiplica Expresion1 y Expresion2 Expresion1 / Expresion2 Divide Expresion1 entre Expresion2

- Expresion El resultado es la Expresion cambiada de signo funcion(lista_parametros) El resultado de la expresión es el valor devuelto por la función

Tabla 4.9

Esta definición se puede aplicar a expresiones de cualquier longitud. Por ejemplo, en la siguiente línea:

IVA = PVP - PVP/1.16;

PVP - PVP/1.16 es una expresión formada por la resta de dos expresiones: PVP y PVP/1.16. Esta última está formada, a su vez, por la división de dos expresiones: PVP y 1.16.

Por último, señalar que una función puede devolver un resultado, esto es, si usamos su nombre en una expresión la función se ejecuta y, cuando termina, tenemos un valor que se usa en el lugar donde la habíamos colocado. La función getchar(), por ejemplo, espera a que el usuario pulse una tecla y devuelve el valor ASCII de la tecla pulsada. En la primera de las sentencias:

char VarCaracter; ... VarCaracter = getchar(); getchar();

estamos usándola en una expresión, de forma que el resultado de la función se almacena en VarCaracter. En la segunda funciona igual, pero el carácter que devuelve no se usa.

Page 15: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 121

Reglas de precedencia y orden de evaluación.

Las reglas de precedencia del lenguaje C nos dicen el orden en que el computador evalúa las distintas operaciones de una expresión cuando ésta contiene varias. Para expresiones aritméticas se siguen, lógicamente, las reglas de precedencia aritmética a las que estamos acostumbrados. Es decir, primero se aplica el operador de cambio de signo (–) , a continuación se calculan multiplicaciones y divisiones, y por último sumas y restas. Cuando hay dos operadores con la misma precedencia, se calcula primero la operación que está a la izquierda. Veamos algunos ejemplos:

IVA = PVP - PVP/1.16;

Tenemos una suma y una división. Siguiendo las reglas de precedencia aritmética, primero se calcula la división y después la suma. Si tenemos:

z = -x + z*2 + n + y/x - 7;

El computador primero cambia de signo el valor de x. Después calcula las multiplicación (los operadores * y / tienen la misma precedencia, pero * está a la izquierda en esta expresión). Sigue con la división y, por último, hace las operaciones de suma y resta empezando por la izquierda de la expresión. Si damos valores concretos a las variables que participan en esta expresión (x,y,z,n) la evaluación se realiza como se indica en la Tabla 4.10.

Orden de la evaluación Acción x=2, y=8, z=3, n=1 Suponemos estos valores iniciales para las variables z=-x + z * 2 + n + y / x - 7 Primero se efectúan las multiplicaciones y divisiones, de

izquierda a derecha (en negrita) z=-x + 6 + n + 4 - 7 Después las sumas y las restas, de izquierda a derecha (en

negrita) z=-2 + 6 + 1 + 4 - 7 Resultado: 2, que se almacenará en z

Tabla 4.10

Se pueden utilizar paréntesis para cambiar la precedencia, al igual que en las expresiones matemáticas. Esto hará que el computador evalúe en primer lugar el contenido de los paréntesis. Los paréntesis se pueden anidar, esto es, se pueden colocar paréntesis dentro otros paréntesis. En tal caso, se calcula en primer lugar el contenido de los paréntesis más internos. Por ejemplo, en

b = (k – z*(n-1))*k;

El computador calcula primero la expresión n–1, sigue con el paréntesis más externo, y lo último que hace es multiplicar su contenido por k.

Orden de la evaluación Acción k=3, z=2, n=5 Suponemos estos valores iniciales para las variables. b = ( k – z * (n-1) ) * k Primero se evalúan los paréntesis más internos, efectuando las

operaciones según las reglas de precedencia que tengan. b = ( k – z * 4 ) * k 2º 1º

Seguidamente, el paréntesis más externo. Dentro de él, se evaluará el producto antes que la suma (en negrita).

b = ( k – 8 ) * k b = -5 * k Por último, el producto. b = -5 * 3 Resultado: -15.

Tabla 4.11

Page 16: capitulo4

122 ♦ Capítulo 4: Fundamentos de Programación.

Cuando tenemos varios paréntesis al mismo nivel de anidamiento, se evalúan empezando la izquierda. El computador calcula la expresión:

z = -(x + z)*(2 + n)+y/(x – 7);

evaluando primero el contenido de los paréntesis antes de hacer las operaciones que hay fuera de ellos.

Un resumen de la prioridad de los operadores aritméticos se puede encontrar en la Figura 4.5 (junto con otros operadores que se explican más adelante). De todas formas, en cuanto se tenga la más mínima duda sobre la precedencia de algún operador sobre otro, recomendamos que se usen paréntesis para forzar y aclarar el orden de las operaciones, evitando así posibles errores en el algoritmo.

Conversión de tipos: reales y enteros.

Generalmente, las variables y constantes que intervienen en una expresión suelen ser del mismo tipo. Sin embargo, el lenguaje C permite utilizar variables y constantes de distinto tipo en una misma expresión. Supongamos que PVPSinIVA y PVPConIVA son variables enteras. La siguiente expresión:

PVPConIVA = PVPSinIVA * 1.16 + 0.5; /* Calcula el precio con IVA redondeando el resultado */

mezcla constantes reales con variables enteras, por lo que el lenguaje C hace una serie de conversiones de

tipo al calcular el resultado. Estas conversiones son necesarias por dos causas:

Los datos de distinto tipo se almacenan con distintos formatos dentro del computador. Si tenemos:

PVPSinIVA = 153.3;

Para almacenar el número real 153.3 en una variable entera hay que convertirlo en entero. En este caso, la conversión de real a entero trunca la parte decimal del número. En general, para una asignación cualquiera:

Nombre_de_variable = Expresion ;

Se modifica el tipo de la expresión para adecuarlo al tipo de la variable.

A veces se mezclan datos o valores de distinto tipo en una operación. Por ejemplo, tengamos en cuenta el siguiente código:

int a = 2; float b = 2.1; b = b*a;

Aquí hay que tomar una decisión sobre el producto de dos variables de distinto tipo (entera y coma flotante). Concretamente en lenguaje C, este producto se realizaría convirtiendo primero el número entero almacenado en a en número flotante, y multiplicando después, con el resultado de 4.2. Si en C se tomara la decisión contraria, es decir, convertir todo a entero y multiplicar, el resultado sería el número entero 4.

La conversión automática de tipos es cómoda para realizar operaciones con variables de distinto tipo mezcladas, pero hay que tener mucho cuidado, sobre todo cuando se mezclan constantes de distinto tipo. Así, algunas operaciones dan resultados distintos dependiendo del tipo de constantes que se usen. Por ejemplo, es evidente que el resultado de (9.0/10.0) es 0.9. Sin embargo el resultado de la expresión (9/10) es 0, puesto que al ser dividendo y divisor números enteros4, la división se efectúa truncando la parte decimal.

La regla de conversión es sencilla si ordenamos los tipos básicos de C en rangos. Para los tipos que hemos visto, la ordenación, de mayor a menor, sería double, float, unsigned int, int, char (como regla mnemotécnica puede servir que: “a mayor número representable con un tipo, más rango en la conversión automática tiene ese tipo”, ver Figura 4.4). Cuando aparecen dos tipos distintos en una operación, se convierten

4 En lenguaje C las constantes con punto decimal se consideran de tipo float y las que no lo tienen de tipo int. Si se

quiere especificar que una constante debe ser tratada como long, se añade la letra L mayúscula al número.

Page 17: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 123

al de mayor rango. En general, antes de realizar una operación entre dos expresiones de distinto tipo, se “promociona” el valor de la expresión de menor rango al mayor:

Expresion1 operador Expresion2 ; /* antes de operar, se convertira la expresion de menor rango a la otra */

En el ejemplo de la Figura 4.3, Pesetas es de tipo int, por lo que en la división:

SonEnEuros= Pesetas/166.386; /* Convierte a euros */

la variable Pesetas se pasa a float antes de ser dividida (la constante 166.386 se considera de tipo float por tener punto decimal).

Aunque la regla es sencilla, hay que tener cuidado con la mezcla de tipos cuando tenemos expresiones complejas, y tener en cuenta que las conversiones se hacen siguiendo la precedencia de las operaciones. Por ejemplo:

VarFloat= 9.0/10.0 + 5.0/7.0; /* El resultado es 1.61428 */

Es equivalente a:

VarFloat= 9/10.0 + 5/7.0; /* El resultado es 1.61428 */

Pero no a:

VarFloat= 9.0/10.0 + 5/7; /* El resultado es 0.9 */

Ya que el orden de evaluación hace que primero se calculen las divisiones, y 5/7 es una división entera que da 0 como resultado. Aunque este resultado pasa a float al hacer la suma, ha perdido ya la parte decimal en la división.

Mayor número representable

double ≅ 10308

float ≅ 1038

unsigned int ≅ 4×109

int ≅ 2×109

unsigned char 255

char 127

Más rango

Menos rango

Figura 4.4.

Operadores enteros.

El lenguaje C tiene algunos operadores que sólo se aplican a expresiones enteras. Pasamos a describirlos brevemente.

El operador módulo:

Expresion1 % Expresion2

Se aplica sólo a expresiones enteras, y da como resultado el resto de la división de Expresion1 entre Expresion2.

Page 18: capitulo4

124 ♦ Capítulo 4: Fundamentos de Programación.

Los operadores de incremento (++) y decremento (--) tienen un comportamiento distinto al de los que hemos visto hasta ahora, puesto que según sea la posición a izquierda o derecha de una variable, la acción que realizan es diferente. Esto se resume en la Tabla 4.12.

Expresión Acción

Nombre_de_variable++ Post-incremento. Devuelve el valor de la variable y luego suma 1 a Nombre_de_variable

++Nombre_de_variable

Pre-incremento. Primero suma 1 a Nombre_de_variable y entonces devuelve el valor ya incrementado.

Nombre_de_variable–– Post-decremento. Devuelve el valor de la variable y después resta 1 a Nombre_de_variable

––Nombre_de_variable Pre-decremento. Resta 1 a Nombre_de_variable y luego devuelve su valor (ya decrementado).

Tabla 4.12

Estos operadores se pueden usar delante o detrás de una variable, y pueden aparecer también ellos solos, sin formar parte de una expresión, en cuyo caso sólo se usa su efecto de incrementar o decrementar la variable a la que acompañan, ignorándose el valor que devuelven como expresión:

Pesetas++; /* Equivale a Pesetas= Pesetas + 1; o ++Pesetas */ --Duros; /* Equivale a Duros= Duros - 1; o a Duros-- */

Cuando se utiliza dentro de una expresión, el lugar donde se coloca el operador puede influir en el resultado. Si va delante de la variable, se incrementa la variable primero, y después se usa el valor incrementado en la expresión. Si va detrás, primero se usa el valor antes de incrementar, y después se incrementa:

x= 1; z= 3*x++; /*se usa x=1 en la expresion, resultando finalmente

z=3 y x=2*/ x= 1; y= 3*++x; /*se incrementa x y se usa en la expresion. Al final

y=6, x=2*/

El operador -- funciona de forma similar a ++, pero restando 1 al contenido de la variable.

No se recomienda usar varios incrementos y/o decrementos de la misma variable en una sola sentencia. Por ejemplo, la expresión:

y = x + x++ * ++x;

es demasiado críptica como para poderse leer con comodidad. Expresiones como ésta inducen a errores de interpretación por parte del programador.

De hecho, el comportamiento con estas expresiones no está estandarizado: algunos compiladores efectúan primero las operaciones de preincremento o predecremento que existan dentro de la expresión, después, evalúan la expresión con los valores actualizados, y por último, realizan todas las operaciones de postincremento y postdecremento que existan. Otros compiladores optan por efectuar las operaciones de postincremento o postdecremento justo después de haber evaluado la variable que acompaña a dichos operadores, en lugar de hacerlo después de evaluar la expresión completa. Esto hace que durante la evaluación de la expresión, una misma variable afectada por estos operadores tenga valores diferentes. En el ejemplo anterior, x es preincrementada una vez y postdecrementada una segunda vez, con lo que llega a tener tres valores diferentes.

Para evitar este tipo de ambigüedades, el programador debería cambiar la expresión anterior, quizá por esta otra:

Page 19: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 125

++x; y = x + x * x; x++;

También se suelen usar mucho los operadores que incrementan o decrementan una variable en el valor de cualquier expresión. Por ejemplo, si se quiere incrementar en 5 el valor de x podríamos escribir:

x = x + 5;

Pero es bastante común encontrar, el operador de incremento general cuyo símbolo es +=. Por tanto, lo anterior produce el mismo resultado que:

x += 5;

La siguiente sentencia:

x = x – (y*2 + 5);

También podría escribirse como:

x -= (y*2 + 5);

La precedencia (prioridad) de todos los operadores aritméticos vistos se resume en la Figura 4.5.

()

++ –– – (operador unario de signo) ~

* / %

+ – (operadores binarios de suma y resta)

& | ^

= += -=

Más precedencia

Menos precedencia

Figura 4.5

Finalmente, en lenguaje C también existen operadores que realizan las operaciones binarias que vimos en el capítulo 1, actuando sobre cada uno de los bits de una variable entera (o char). La Tabla 4.13 resume estos operadores y un ejemplo para ellos.

Símbolo del operador binario

Operación lógica equivalente (puerta)

Ejemplo

& AND 5 & 3 dará 1 (en base 2: 101 AND 011 es 001) | OR 5 | 9 dará 13 (en base 2: 0101 OR 1001 es 1101) ^ XOR 5 ^ 9 dará 12 (en base 2: 0101 XOR 1001 es 1100) ~ NOT ~(0xFE) de 8 bits es 0x01 (en base 2: NOT(1111 1110) es 0000 0001)

Tabla 4.13

Page 20: capitulo4

126 ♦ Capítulo 4: Fundamentos de Programación.

4.3 ESTRUCTURAS CONDICIONALES.

Por defecto, las sentencias de un programa C se ejecutan secuencialmente, empezando por la primera de la función main, y ejecutando una a una las siguientes hasta llegar a la última. En este apartado y en el siguiente veremos cómo se puede alterar tal comportamiento, haciendo que se ejecute parte de un programa sólo si se dan determinadas condiciones, es decir, variando el flujo de ejecución. Esto es necesario para poder escribir la mayoría de los algoritmos. Precisamente por esto, a lo largo de la historia de la computación y de los lenguajes algorítmicos se han ido proponiendo una serie de representaciones algorítmicas, que pudieran expresar no sólo expresiones aritméticas sino variaciones en el flujo de ejecución. Las más importantes se estudian en la siguiente sección.

4.3.1 Representaciones algorítmicas

Un programa o algoritmo donde la ejecución exhibe un comportamiento secuencial no tiene necesidad de ser representado, ya que su ejecución seguirá siempre un orden invariable: empezar por la primera sentencia y ejecutar una a una las siguientes hasta llegar a la última. Una forma típica de representación es el diagrama de flujo, donde se dibuja con una línea o flecha el hilo o flujo por donde debe de ir ejecutándose nuestro programa. Cada sentencia se suele dibujar con una caja o rectángulo. En un programa con comportamiento secuencial, el diagrama de flujo será sencillamente el que vemos en la Figura 4.6a.

Sentencia 1

Sentencia 2

Sentencia 3

Última sentencia

NO

SI

¿Condición cierta?

Sentencia n

Sentencia Condicionada 1

Sentencia n+1

Última Sentencia Condicionada k

NO

SI

¿Condición cierta?

Sentencia n

Sentencia 1 (caso SI)

Sentencia n+1

Última Sentencia k (caso SI)

Sentencia 1 (caso NO)

Última Sentencia m (caso NO)

Figura 4.6a. Figura 4.6b. Figura 4.6c.

Sin embargo, para expresar muchos algoritmos es necesario que el flujo de ejecución tenga ramificaciones, es decir que existan sentencias especiales donde la ejecución puede tomar dos rumbos o caminos. Por ejemplo en la Figura 4.6b, se observa un programa que tras ejecutar n sentencias, llega a una sentencia especial (en forma de rombo) donde una pregunta o condición decide si se han de ejecutar un conjunto de k sentencias (cajas con línea discontinua) condicionadas por la pregunta de la anterior. Este cambio del flujo de ejecución puede también decidir entre ejecutar un conjunto de k sentencias cuando la condición es falsa, o ejecutar otro conjunto de m sentencias si aquélla fuera cierta (ver la Figura 4.6c). Por supuesto, pueden existir distintos puntos del programa donde se rompa la ejecución secuencial.

Page 21: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 127

Para fijar ideas veamos un sencillo ejemplo expresado con este tipo de diagramas. El algoritmo de la Figura 4.7 calcula si un número es divisible por 2. Para calcular si un número es divisible por otro, usaremos el operador módulo (%), que devuelve el resto de la división, con lo que el número es divisible si tal resto es igual a cero.

Pedir número y almacenarlo en la

variable n

FIN

Imprimir: “El número no es divisible por 2.”

NO

SI

¿n%2 es 0?

Imprimir: “El número es divisible por 2”

Figura 4.7

Como vemos en la Figura 4.7, en la condición se comprueba si el número es divisible por 2. Si el módulo 2 de n es igual a cero, la rama tomada sería la del “SI” y se imprimiría el mensaje: “El número es divisible por 2”. En caso de que el módulo 2 de n sea distinto a 0, la rama tomada sería la del “NO”, y se imprimiría el mensaje: “El número no es divisible por 2”. En ambos casos, después de la impresión, se conducirá la ejecución hacia el final del programa por el mismo camino.

Además de la representación por medio de diagrama de flujo, existe otra muy usada y que tiene la ventaja de ser más compacta (en el sentido de que ocupa menos espacio) que la anterior: el pseudocódigo. Se trata de escribir el algoritmo con palabras en el idioma natural de cada uno, incluyendo las expresiones matemáticas que uno considere oportunas, pero sin la necesidad de mantener una exactitud y formalidad en cuanto a las declaraciones de variables, la sintaxis, etc. En un pseudocódigo, el programador tan sólo debe centrarse en la veracidad del algoritmo. La traducción de pseudocódigo a un lenguaje imperativo suele ser generalmente fácil, y sólo han de tenerse en cuenta consideraciones formales y sintácticas para que el programa definitivo (escrito en lenguaje C, por ejemplo) sea correcto. Por ejemplo, el algoritmo de la Figura 4.7, podría escribirse como:

Pedir un número y almacenarlo en una variable n. Si n es divisible por 2: Imprimir mensaje: “El número es divisible por 2” Pero si no: Imprimir mensaje: “El número no es divisible por 2” Fin del programa.

El pseudocódigo anterior también se podría haber escrito incluyendo operadores matemáticos, pero, en lo que respecta a la comprensión del flujo de ejecución, esto no es necesario, y puede incluso distraer la atención del programador cuando éste tenga que expresar algoritmos más complejos.

La única consideración a tener en cuenta en el pseudocódigo anterior es la forma de escribir las ramificaciones. Nótese que si la pregunta (que empieza por Si) es cierta, se ejecutan una serie de sentencias que se han escrito más a la derecha (se han tabulado para separarlas del resto). Pero si la pregunta no es cierta, entonces existe una parte del pseudocódigo que empieza por Pero si no, y también en ese caso se ejecutan una

Page 22: capitulo4

128 ♦ Capítulo 4: Fundamentos de Programación.

serie de sentencias que se tabulan más a la derecha. Evidentemente, ante una pregunta (cuya respuesta sólo puede ser SI o NO), solamente se va a ejecutar una de las partes identadas.

Como hemos visto en este sencillo ejemplo la existencia de preguntas o toma de decisiones (llamadas en general estructuras condicionales) es vital para poder escribir algoritmos, y cada lenguaje define un conjunto de las mismas. En el siguiente apartado explicaremos las estructuras condicionales del lenguaje C: if-else y switch, dejando otras que también rompen el flujo secuencial de ejecución (estructuras iterativas) para un apartado posterior.

4.3.2 La estructura if-else.

La sentencia if (del inglés “si”) evalúa cierta pregunta, y en caso afirmativo, ejecuta un trozo de código. En caso negativo se ejecutaría otro trozo de código precedido por la palabra else, (en inglés “de otra forma”, “en caso contrario”). En primer lugar vamos a trabajar con estructuras condicionales donde el código que se ejecuta tras el if o el else se compone de una única sentencia.

El formato general es:

if (Condicion) Sentencia1; else Sentencia2;

Su funcionamiento es muy simple: si la condición es verdadera, se ejecuta Sentencia1, si es falsa se ejecuta Sentencia2. A continuación vemos un posible código real:

if (PtsOEuros == 'E') printf("Cuantos euros ? "); else printf("Cuantas Pesetas ? ");

La condición es verdadera si el valor de PtsOEuros es igual a la constante 'E'.

Una forma más simple de estructura condicional es la estructura if. Es una simplificación de if-else, que no contiene la parte else (la que se ejecutaría si la condición fuera falsa). Su formato general es:

if (Condicion) Sentencia1;

En este caso, si se cumple la condición se ejecuta Sentencia1, en caso contrario se continúa con el código tras el if.

El formato de las sentencias y estructuras en C es totalmente libre. De hecho, podemos colocar la estructura if-else en una sola línea:

if (PtsOEuros=='E') printf("Euros ?"); else printf("Pesetas?");

Sin embargo, aunque no es obligatorio, en las estructuras condicionales se suele usar tabulación del texto, que consiste en comenzar las sentencias incluidas en la estructura if-else en líneas separadas, que comienzan algunas columnas después que el resto de las sentencias (generalmente se usa el tabulador para desplazar esas columnas a la derecha). De esta forma es más fácil ver la estructura condicional y cuáles son las sentencias que la componen.

Para ver su utilidad y la forma de escribirla en C, vamos a analizar un programa que nos permite pasar de pesetas a euros o viceversa:

Page 23: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 129

#define PTS_POR_EURO 166.386 main() { float CantidadOriginal, CantidadConvertida; char PtsOEuros; /* Pide al usuario que elija si convertir pts o euros */ printf("Introduce P si quieres convertir pesetas, "); printf(" o E si quieres convertir euros:"); PtsOEuros=getchar(); /* Espera a que introduzca un caracter */ PtsOEuros=toupper(PtsOEuros); /*Pasa a mayusculas el caracter*/ /* El mensaje que muestra depende de PtsOEuros */ if (PtsOEuros == 'E') printf("Cuantos euros? "); else printf("Cuantas Pesetas? "); scanf("%f", &CantidadOriginal); /* Lee la suma a convertir */ if (PtsOEuros == 'E') /* el calculo depende de PtsOEuros */ CantidadConvertida= CantidadOriginal*PTS_POR_EURO; else CantidadConvertida= CantidadOriginal/PTS_POR_EURO; if (PtsOEuros == 'E') /* El mensaje depende de PtsOEuros */ /* muestra el resultado con 3 decimales */ printf("Son %.3f pesetas\n",CantidadConvertida); else printf("Son %.3f Euros\n",CantidadConvertida); }

Lo primero que hace este programa es pedirle al usuario que seleccione si quiere convertir pesetas o euros (introduciendo la letra ‘p’ o la ‘e’, respectivamente). La letra se introduce en la variable PtsOEuros (la función estándar getchar() la recoge del teclado). Ya sólo restaría comprobar el valor de esta variable y en función del mismo realizar una acción u otra. Para esta discriminación de la acción a realizar usaremos la estructura if-else.

4.3.3 Expresiones de condición.

Hasta ahora hemos visto expresiones aritméticas, cuyo resultado puede ser cualquier valor numérico (entero o real según los operandos que contenga). Pero la condición de una sentencia condicional (como el if-else), sólo puede dar uno de dos resultados: verdadero (cierto) o falso.

Las expresiones de condición más sencillas son las comparaciones. Estas comparan valores de expresiones aritméticas, dando como resultado cierto si se cumple la comparación y falso en caso contrario. El formato general es:

Expresion1 op_cmp Expresion2

donde op_cmp es un operador de comparación, como “es igual a”, “es mayor que”, etc. Los símbolos y tipos de operadores de comparación se muestran en la Tabla 4.14.

Page 24: capitulo4

130 ♦ Capítulo 4: Fundamentos de Programación.

Expresión condicional de comparación Condiciones para que la expresión sea cierta

Expresión1 == Expresión2 Expresion1 es igual a Expresion2 Expresión1 != Expresión2 Expresion1 es distinta de Expresion2 Expresión1 > Expresión2 Expresion1 es mayor que Expresion2 Expresión1 < Expresión2 Expresion1 es menor que Expresion2

Expresión1 >= Expresión2 Expresion1 es mayor o igual que Expresion2 Expresión1 <= Expresión2 Expresion1 es menor o igual que Expresion2

Tabla 4.14

Varias condiciones se pueden combinar para formar expresiones de condición más complejas. En el siguiente ejemplo:

if (PtsOEuros!='E' && PtsOEuros!='P') printf("\nError: No se ha pulsado una P ni una E.\n"); else /* ...continuar con el programa */

Se deben dar dos condiciones para ejecutar printf(): que PtsOEuros sea distinto de 'E' y que sea distinto de 'P'. Pruebe a introducir esta condición en el programa de ejemplo del apartado 4.3.2 , y tendrá un mejor programa.

Por otro lado, los operadores lógicos permiten combinar condiciones. Como en las reglas de la lógica matemática o lógica digital, existen dos operadores lógicos de condición: Y(AND) y O(OR), cuyos símbolos son para las expresiones condicionales de lenguaje C: && y ||. Su formato se resume en la Tabla 4.15.

Expresión condicional con operadores lógicos Condiciones para que sea cierta Condicion1 && Condicion2 Condicion1 es cierta Y Condicion2 es cierta Condicion1 || Condicion2 Condicion1 es cierta O Condicion2 es cierta

! Condicion Condición NO es cierta

Tabla 4.15

Por último, una propia expresión aritmética puede convertirse en condición. La regla es: si el valor de la expresión es distinto de cero, entonces sería interpretada como condición cierta; pero si la expresión resulta ser nula, entonces sería falsa. Por ejemplo, en el siguiente fragmento de código, la condición del if sólo será falsa si n es cero:

int n; if (n*n) printf (“Condicion cierta\n”); /* se ejecuta siempre

que n no sea 0*/ else printf (“Condicion falsa\n”); /* esto solo se ejecuta

si n es 0*/

Para concluir y recapitular, veamos algunos ejemplos y el resultado de su condición:

Page 25: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 131

Expresión condicional Resultado 7 < 10 && 9 > 10 Falsa, pues sólo la primera condición es verdad. 7 < 10 || 9 > 10 Cierta, pues sólo es necesario que una de las condiciones sea cierta. !(9 > 10) Cierta, porque 9 no es mayor que 10 X >= 15.1 && X <= 35.2 Cierta si X está dentro del rango [15.1, 35.2] !(X >= 15.1 && X <= 35.2) Cierta si X está fuera del rango [15.1, 35.2] Ent % 3 Cierta si Ent no es divisible por 3 (el resto de la división no es cero). Ent%3 == 0 || Ent%2 == 0 Cierta si Ent es divisible por 3 ó por 2 (el resto de la división es cero). char letra; Letra=='E' || Letra=='e'

Cierta si Letra contiene una ‘E’ mayúscula o minúscula, (es decir, su ASCII correspondiente).

Tabla 4.16

Como ejercicio puede utilizar la última línea de estos ejemplos, para eliminar la función estándar toupper() en el programa del apartado 4.3.2, modificando las líneas del estilo:

if (PtsOEuros == 'E')

4.3.4 Precedencia en las expresiones condicionales.

Como en las expresiones aritméticas, el lenguaje C usa una serie de reglas para determinar el orden en que se realizan las operaciones de una expresión condicional:

Si hay operadores aritméticos, evalúa primero las expresiones aritméticas. De los operadores condicionales, los de comparación tienen la mayor precedencia (se evalúan

en primer lugar), seguidos de && y ||. Los paréntesis alteran el orden de precedencia también en las expresiones condicionales, de

forma que siempre se evalúan en primer lugar las expresiones en el nivel de paréntesis más interno.

La precedencia de ! es un tanto especial: está por encima de la multiplicación e inmediatamente por debajo de los paréntesis.

Normalmente, las expresiones con operadores se evalúan de izquierda a derecha, aunque no todos: ciertos operadores se evalúan y se asocian de derecha a izquierda. Además no todos los operadores tienen la misma prioridad, algunos se evalúan antes que otros. De hecho, existe un orden muy concreto en los operadores en la evaluación de expresiones. Esta propiedad de los operadores se conoce como precedencia o prioridad.

Veremos ahora las prioridades de todos los operadores, incluidos los que aún no conocemos. Considere esta tabla como una referencia, no es necesario aprenderla de memoria. En caso de duda siempre se puede consultar, incluso puede que cambie ligeramente según el compilador.

Page 26: capitulo4

132 ♦ Capítulo 4: Fundamentos de Programación.

Operadores Asociatividad () [] -> :: . Izquierda a derecha Operadores unitarios: ! ~ + - ++ -- & (direccion de) * (puntero a) sizeof new delete

Derecha a izquierda

.* ->* Izquierda a derecha * (multiplicacion) / % Izquierda a derecha + - (operadores binarios) Izquierda a derecha << >> Izquierda a derecha < <= > >= Izquierda a derecha == != Izquierda a derecha & (bitwise AND) Izquierda a derecha ^ (bitwise XOR) Izquierda a derecha | (bitwise OR) Izquierda a derecha && Izquierda a derecha || Izquierda a derecha ?: Derecha a izquierda = *= /= %= += -= &= ^= |= <<= >>= Derecha a izquierda , Izquierda a derecha

Tabla 4.17

La Tabla 4.17 muestra las precedencias de los operadores en orden decreciente, los de mayor precedencia en la primera fila. Dentro de la misma fila, la prioridad se decide por el orden de asociatividad.

La asociatividad nos dice en qué orden se aplican los operadores en expresiones complejas, por ejemplo:

a=b=c=5

El operador de asignación "=" se asocia de derecha a izquierda, es decir, primero se aplica "c=5", después "b=c", etc. O sea, a todas las variables se les asigna el mismo valor: 5.

En el siguiente ejemplo, el operador * tiene mayor precedencia que + e =, por lo tanto se aplica antes. Después se aplica el operador +, y por último el =.

L = K * M + N * P;

Otro ejemplo, puede ser la siguiente expresión:

N > 7 && !(K == 6 || Z < N – 1)

En este caso, el orden de evaluación es:

Comparación N > 7. Contenido de los paréntesis (K == 6 || Z < N – 1).

Operación aritmética N – 1 . Comparaciones; primero K == 6 y después Z < N – 1. Operación lógica K == 6 || Z < N – 1.

Operación lógica !(...). Operación lógica N > 7 && !(...).

También aquí se pueden utilizar y anidar paréntesis para cambiar la precedencia de las expresiones condicionales, al igual que en las expresiones aritméticas. Así, aunque el lenguaje C tiene perfectamente definidas las precedencias de los operadores, desde aquí aconsejamos el uso de paréntesis en caso de duda, para así exigir el orden requerido en la ejecución de las operaciones.

Page 27: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 133

4.3.5 Bloques.

Un bloque es un conjunto de código C entre llaves { }. Por ejemplo, si en el caso de que una sentencia if (sea cierta o falsa), se quieren ejecutar varias sentencias en vez de una sola, pueden encerrarse todas entre llaves (un bloque). Cuando se evalúe la condición del if se ejecutará todo ese bloque (de forma secuencial) en caso de condición cierta, o el del else en caso de falsedad. De esa forma el diagrama de flujo será exactamente el de la Figura 4.6.c, es decir:

Condicióncierta

Condiciónfalsa /* else* /

IF

{ Sentencias entre llaves; }

{ Sentencias entre llaves; }

Figura 4.8

Así, un bloque se trata de la misma forma que una sentencia a la hora de construir estructuras if-else o cualquier otra estructura del lenguaje. En el siguiente ejemplo, hemos reorganizado todas las sentencias de las estructuras if-else del programa del apartado 4.3.2, para formar los bloques de un único if-else:

if (PtsOEuros == 'E') { /* { indica el comienzo de un bloque */ printf("Cuantos euros ? "); scanf("%f", &CantidadOriginal); CantidadConvertida= CantidadOriginal*PTS_POR_EURO; printf("Son %.3f pesetas\n",CantidadConvertida); } /* } indica el final de un bloque.

Ojo, no se debe colocar ; al final de un bloque */ else { /* Comienzo del segundo bloque */ printf("Cuantas Pesetas ? "); scanf("%f", &CantidadOriginal); CantidadConvertida= CantidadOriginal/PTS_POR_EURO; printf("Son %.3f Euros\n",CantidadConvertida); } /* Fin del segundo bloque */

Cada bloque está formado por las sentencias incluidas entre las llaves { }. Dentro de un bloque puede haber cualquier número de sentencias, e incluso otros bloques:

Page 28: capitulo4

134 ♦ Capítulo 4: Fundamentos de Programación.

int Numero, N_entre2, N_entre4; ... if (Numero % 2 == 0) { printf("Numero Par.\n "); N_entre2 = Numero/2; if (N_entre2 % 2 == 0) { /* bloque anidado dentro de otro */ printf("Y divisible entre 4.\n "). N_entre4 = N_entre2/2; } }

Esto nos permite realizar estructuras if-else anidadas, es decir, que están dentro de otras estructuras if-else.

En este ejemplo vemos que la tabulación se utiliza para hacer más visible la estructura del if compuesto, haciendo que las sentencias que están en niveles de anidamiento distinto comiencen en columnas distintas. Cuanto mayor es el nivel, más a la derecha comienza una sentencia. El lenguaje C no obliga a esta organización de las sentencias; de igual forma que tampoco obliga a colocar comentarios: son técnicas opcionales que hacen más fácil de entender un programa y que aquí recomendamos enérgicamente.

En lo que sigue, cuando hablemos de forma genérica de una <sentencia>, ésta puede estar compuesta por todo un bloque entre llaves o por sólo una línea de código.

4.3.6 Selecciones múltiples. La estructura switch.

La estructura if-else permite elegir entre dos posibles bloques de acciones en función de una sola condición. Cuando queremos realizar una selección entre más de dos bloques en función de más de dos condiciones, podemos utilizar dos construcciones del lenguaje C especialmente pensadas para este tipo de situaciones: anidar estructuras if-else-if y la estructura switch. Se denominan estructuras de selección múltiple, y el ejemplo más claro y típico de ellas se da cuando en función del valor de una variable se elige entre una serie de opciones de un menú. Siguiendo con la conversión de monedas en euros, imaginemos un menú que pregunta al usuario a qué tipo de divisa quiere convertir una cantidad en euros; un menú del estilo de:

Elija una opcion: - Pulse F para convertir de euros a francos. - Pulse M para convertir de euros a marcos. - Pulse L para convertir de euros a liras.

Page 29: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 135

DEMULTIPLEXOR

SI

NO

¿tecla ‘F’? Realizar

conversión de euros a francos

SI

NO

¿tecla ‘M’? Realizar

conversión de euros a marcos

SI

NO

¿tecla ‘L’? Realizar

conversión de euros a liras

Informar de que no se ha

elegido ninguna opción válida

‘F’ ‘M’ ‘L’ CUALQUIER OTRA

Realizar conversión de

euros a francos

Realizar conversión de

euros a marcos

Realizar conversión de euros a liras

Informar de que no se ha

elegido ninguna opción válida

Figura 4.9

Es evidente que tras elegir una de las tres opciones, habrá que decidir qué conversión hacer (diferente para cada moneda) en función de lo elegido. También parece razonable que la tecla pulsada se almacene en una variable tipo char, y en función de su valor se decida qué hacer. El contenido de tal variable podrá ser ‘F’,’M’, ‘L’ u otra tecla diferente, y la decisión será una selección múltiple.

El flujo de ejecución de una estructura de este tipo puede obedecer a uno cualquiera de los mostrados en la Figura 4.9.

En general, los algoritmos donde es necesario usar una estructura de selección múltiple, suelen ser aquellos donde hay que distinguir entre varios valores o rangos de valores de una variable, y en función del rango o valor que toma, realizar distintas acciones. Un ejemplo de este tipo se analiza en la siguiente sección.

La estructura if-else-if.

Esta estructura consiste básicamente en encadenar uno o más if-else al final de otra estructura if-else. El siguiente ejemplo utiliza varias comparaciones para saber si la variable Car contiene una letra mayúscula, minúscula o un dígito:

char Car; ... if (Car >='A' && Car <='Z') printf("Es una letra mayuscula.\n"); else if (Car >='a' && Car <='z') printf("Es una letra minuscula.\n"); else if (Car >='0' && Car<='9') printf("Es un digito.\n"); else printf("Ni mayuscula, ni minuscula, ni digito.\n");

Si se cumple la primera condición, se ejecuta el primer printf(), en caso contrario, se pregunta por la siguiente condición. Si se cumple, se ejecuta el segundo printf(). Si no, se pregunta por la tercera, y así hasta el final de la cadena. Si no se cumple ninguno de los casos u opciones, se entra en el último else. Nótese en este ejemplo que las comparaciones que se hacen funcionan correctamente, porque el ASCII de las letras es consecutivo según el orden alfabético, (el ASCII de la ‘A’ es 65, de la ‘B’ 66, ‘C’ 67, …, y el de la ‘Z’ es el 90). Ídem para los dígitos del ‘0’ al ‘9’.

Page 30: capitulo4

136 ♦ Capítulo 4: Fundamentos de Programación.

Este tipo de selección múltiple se puede ampliar utilizando bloques de acciones entre llaves {}, en lugar de una sola acción por cada if o else. Incluso se pueden crear otras estructuras de selección más complejas, si el programador pone las estructuras de condición que necesite. Por ejemplo:

if (Condicion_1) { /* varias sentencias */ if (Condicion_2) { /* otras sentencias */ } else { if (Condicion_3) /* una sentencia */; } /* mas sentencias */ } else { /* diferentes sentencias */ }

Sin embargo, este tipo de estructuras no obedece a una estructura regular de selección múltiple, sino que surge de condiciones más complicadas que no suelen aparecer en los sencillos algoritmos habituales y no entraremos aquí en más detalle.

La estructura switch.

La estructura que suele ser típica en muchos programas es la selección tipo menú entre varias opciones, como el ejemplo anterior de la conversión de euros a diferentes divisas. Siguiendo con ese ejemplo, imaginemos que la presentación del menú se construye con:

printf("Elija una opcion:\n"); printf("- Pulse F para convertir euros a francos.\n"); printf("- Pulse M para convertir euros a marcos.\n"); printf("- Pulse L para convertir euros a liras.\n"); scanf (“%c”, & OpcionElegida); printf("Introduzca cantidad para convertir: \n"); scanf (“%f”, &CantidadOriginal);

Entonces podemos usar la estructura switch para seleccionar entre varias alternativas dependiendo del valor de una expresión aritmética o de tipo carácter. Es decir, la estructura switch se corresponde perfectamente con el diagrama de flujo de la parte derecha de la Figura 4.9. El ejemplo anterior se completaría con:

Page 31: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 137

switch (OpcionElegida) { case 'F': CantidadConvertida= CantidadOriginal*FRANCOS_POR_EURO; printf("Son %.3f francos\n",CantidadConvertida); break; case 'M': CantidadConvertida= CantidadOriginal*MARCOS_POR_EURO; printf("Son %.3f marcos\n",CantidadConvertida); break; case 'L': CantidadConvertida= CantidadOriginal*LIRAS_POR_EURO; printf("Son %.3f liras\n",CantidadConvertida); break; default: printf("Error: debe pulsar F, M o L.\n"); }

El compilador evalúa en primer lugar el valor de la expresión contenida en switch, en este caso OpcionElegida. A continuación lo compara con cada una de las constantes contenidas en las opciones case (en inglés “caso”). Si vale 'F', por ejemplo, ejecuta todas las sentencias hasta el siguiente case, ya que al final de cada caso se ha colocado una sentencia de “ruptura” de ejecución: break. Si OpcionElegida no vale 'F', se sigue comparando con las siguientes opciones case hasta que coincida con alguna o se llegue al final. Por último, si no hay ninguna coincidencia, se ejecutan las sentencias contenidas en la opción default (que podríamos traducir como “en su defecto”).

Es importante colocar la sentencia break al final de cada opción case. Si no se colocara ninguna, el computador seguiría ejecutando las acciones de las siguientes opciones, y por último las del default. Por ejemplo, si OpcionElegida vale 'F', y no se coloca break, se ejecutan las sentencias correspondientes a case 'F', y después continuaría con las siguientes sentencias que se incluyan en el switch, como si se tratase de un trozo de código normal. La salida por pantalla mostraría los cuatro printf(), con tres conversiones y el último mensaje “Error:…”.

4.4 ESTRUCTURAS ITERATIVAS.

La palabra iteración significa repetición, reiteración; por lo tanto, en este apartado vamos a estudiar las estructuras que implican repetir o iterar un fragmento de código, y las sentencias que se definen en lenguaje C para ello.

Aunque aún no hayamos hablado de este tipo de estructuras, su necesidad resulta casi evidente en múltiples algoritmos. De hecho son las estructuras más importantes en los lenguajes imperativos, como el lenguaje C que estamos estudiando.

Imaginemos por ejemplo que queremos saber si un número natural n es primo. La solución de esta pregunta, según la definición de número primo, implica comprobar si la división de n por los números menores que él, es exacta o no (es decir, si el resto del cociente es cero o no). Por lo tanto, el código que resolvería esta pregunta implica la necesidad de una estructura iterativa o reiterativa que vaya repitiendo la división desde 2 hasta n-1. Si se quiere refinar el algoritmo para evitar divisiones innecesarias, en realidad sólo sería necesario comprobar el resto de la división por 2 y por los números impares desde 3 hasta el número impar inmediatamente inferior que la raíz cuadrada de n. Aún más fino, sería tener una lista de números primos menores que la raíz cuadrada de n, y comprobar solamente los cocientes entre n y tales números primos.

Otros casos que nos exigen disponer de estructuras iterativas son, por ejemplo, aquellos donde un problema matemático se resuelve repitiendo una misma operación. En general, tal es el caso de las expresiones matemáticas que se escriben con un sumatorio. Así, el cálculo de la media aritmética de un conjunto de números puede resolverse almacenando la suma de los números en alguna variable, y finalmente dividir por la cantidad de números que componen la lista:

Page 32: capitulo4

138 ♦ Capítulo 4: Fundamentos de Programación.

N por división más iterativa EstructuraxN

xN

ii ⇔= ∑

=1

1

Ecuación 4.1

Otro ejemplo más sofisticado puede ser calcular el valor aproximado de la integral de cierta función f(x) entre dos límites a y b. Para ello, podemos usar la semejanza con la superficie que queda por debajo de la función (ver Figura 4.10). Entonces, se puede iterar acumulando la suma de los pequeños rectángulos de altura f(x) y base ∆x, desde a hasta b.

f(x)

a x x+∆x b x

( )∑∫ ∆≈ xxfdxxfb

a)()(

Figura 4.10

En el campo del tratamiento y gestión de datos, es evidente que los procesos repetitivos son necesarios. Imaginemos, por ejemplo, la gestión de las cuentas de ahorro de un banco, donde en general los procesos realizados consisten en repetir la misma operación para todos sus clientes (por ejemplo calcular los intereses a final de mes, restar las comisiones de mantenimiento, etc.). Análogamente, hoy en día son muy frecuentes los algoritmos iterativos en las llamadas aplicaciones “multimedia”, como las que se realizan con imágenes y sonidos. Pongamos por caso un algoritmo que modifique el tono de color en cada uno de los puntos de una imagen (denominados “pixels”). Es obvio que tal algoritmo recorrerá cada uno de los puntos de la imagen, repitiendo la misma operación sobre ellos, es decir, usando algún tipo de estructura iterativa. Y así un sinfín de cálculos matemáticos y algoritmos que se pueden escribir fácilmente usando una estructura iterativa.

De hecho, las estructuras iterativas son tan comunes en los programas escritos en un lenguaje imperativo, que se las suele denominar con el más sencillo nombre de “bucles” (en algunos textos como ciclos o lazos). Por tal motivo, a partir de ahora nos referiremos a las mismas con ese nombre. Además, los bucles o estructuras iterativas no sólo son necesarios en casos donde se tenga que realizar un proceso iterativo, sino que nos permiten escribir de forma más cómoda y concisa trozos de código que resultarían largos y pesados de otra forma. Por ejemplo, sería pesado y largo escribir:

x=a*a* a*a* a*a* a*a;

Mientras que un buen programador acaba por acostumbrarse a escribir de forma más simple algo como (en pseudocódigo):

x=1.0; expon=8; repite expon veces x=x*a;

Que equivaldría literalmente a escribir lo siguiente:

Page 33: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 139

x=x*a; x=x*a; x=x*a; x=x*a; x=x*a; x=x*a; x=x*a; x=x*a;

La expresión en forma de bucle es más genérica y permitiría calcular cualquier potencia entera no negativa de a con sólo cambiar el valor de la variable expon.

La forma general con la que se debe trabajar en un bucle es, salvo excepciones, la que se muestra en la Figura 4.11. En tal figura, se observa cómo nuestro código, al encontrarse con un bucle, primero debe preguntar si se han acabado los elementos a procesar, es decir, si ya no quedan iteraciones por realizar. Es evidente por tanto, que el bucle debe llevar implícita una estructura condicional, como las vistas en el apartado anterior. Si la respuesta es afirmativa, el bucle no realizaría ninguna iteración, pero si es negativa, se pasará a realizar la primera iteración. En ella se ejecutará una primera vuelta, donde se leerá y procesará un primer elemento, y finalmente se actualizará el número de iteraciones que quedan o de elementos que faltan por procesar. El proceso se repite cíclicamente, pasando a la segunda pregunta, y así hasta que los elementos se agoten, es decir, no haya más iteraciones que ejecutar.

Viene

NO

¿Quedanelementos por

iterar?

SI Lee y procesaelementosiguiente

Actualizanúmero deelementosprocesados

Continúa

Figura 4.11

Como puede observarse, la pregunta “¿Quedan elementos por iterar?” juega un papel fundamental en el bucle, y hay que saber elegir la variable o condición adecuada para la misma. Un error en la formulación de tal pregunta conduciría a que el bucle realizara un número de iteraciones no esperado, con lo que el mismo no ejecutaría correctamente el algoritmo deseado. Algunas de estas condiciones pueden ser simples, por ejemplo cuando el bucle debe realizar un número fijo de iteraciones, llamémosle N. Entonces, declarando inicialmente una variable con el valor 0, preguntado en la condición si la variable ha alcanzado el valor N, e incrementando en 1 el valor de la misma tras procesar cada iteración, tendríamos construido el bucle de forma correcta. Tal esquema se muestra en la Figura 4.12, usando la variable i. La variable que realiza la cuenta de iteraciones se suele llamar “contador” o “índice” del bucle. Por razones históricas y relacionadas con las matemáticas, las variables que se suelen usar como contadoras en los bucles suelen ser i, j, k, cuando son enteras5, dejando las últimas letras del alfabeto para las reales (x, y, z). Aunque, como sabemos, en lenguaje C cualquier variable puede ser declarada de cualquier tipo y jugar el papel que el programador quiera, en este texto seguiremos, en general, tal convenio.

5 Téngase en cuenta que los subíndices de los sumatorios (que se codifican como bucles) suelen ser precisamente i, j, k.

Page 34: capitulo4

140 ♦ Capítulo 4: Fundamentos de Programación.

i=0

NO

¿i<N?SI Lee y procesa

elementosiguiente

i=i+1

Continúa

Figura 4.12

Sin embargo la condición para seguir iterando un bucle, no siempre es tan simple como la de las figuras anteriores, sino que el número de iteraciones que debe realizar un bucle puede no ser constante ni estar contenido en una variable, o incluso ser desconocido cuando se escribe el programa. El caso más claro de esta situación se da cuando la condición depende de los valores que introduce el usuario que está ejecutando nuestro programa. Un simple ejemplo de tal situación se muestra en la Figura 4.13, donde se ejecuta un fragmento de código que calcula la raíz cuadrada mientras el número (que se almacena en la variable x) que introduce por teclado el usuario sea positivo o nulo. Cuando el número introducido sea negativo, se sale del bucle, y el programa continúa.

Leer número delteclado en x

NO

¿x>=0?SI Imprime en

pantalla la raízcuadrada de x

Continúa

Figura 4.13

Por ello, en el lenguaje C se dispone de estructuras iterativas bastante genéricas, de forma que los bucles con condiciones simples como el de la Figura 4.12, se reducen a casos particulares de los mismos. En las próximas secciones se irán detallando algunos ejemplos de bucles, eligiendo la condición más adecuada, y usando las estructuras iterativas de que dispone el lenguaje C: los bucles while, do...while y for.

4.4.1 El bucle while

La palabra “while” significa en inglés “mientras”. Es obvio que una estructura iterativa while contiene por tanto, una pregunta o condición6 de finalización del bucle del estilo “mientras ocurra tal condición continúa iterando”. El pseudocódigo de un bucle que se escriba con esta sentencia sería del estilo:

6 Esta condición es en realidad cualquier expresión que devuelva un valor numérico entero. Un valor de la expresión igual a 0

hace que la condición se evalúe como falsa, y cualquier otro valor, distinto de 0, hace que se evalúe como verdadera.

Page 35: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 141

Inicialización; Mientras (condición) { Operaciones; }

Dentro de las operaciones del bucle lo normal será encontrarse alguna sentencia que actualice la condición del bucle. En lenguaje C la forma de escribir un bucle while (la sintaxis) es similar:

while (condicion) <sentencia>;

Como siempre, la sentencia puede ser una única línea de código o un bloque entero encerrado entre llaves. La sentencia sería el “cuerpo” del bucle, y en general contendrá la actualización necesaria de la condición que se evaluará en el while.

Por ejemplo, para escribir un bucle que debe iterarse un número N fijo de veces (como en la Figura 4.12) usando un bucle while se debe recurrir a una variable que actúe de “contador”, y que se vaya incrementando dentro del cuerpo del bucle. Así la traducción a C de tal figura sería:

i=0; while (i<N) { <operaciones a realizar>; i++; }

Como se observa claramente, en la primera iteración la variable que hace de contador, i, tiene el valor 0, de forma que se ejecutaría una primera iteración (suponiendo que N es distinto de cero). Al final de la misma se incrementa el valor de i, y entonces se vuelve hacia atrás, donde se evalúa la condición del bucle while, comparando i con N. Así se repetirían las operaciones del cuerpo del bucle, hasta que i alcance el valor de N (N iteraciones). Hay que advertir aquí que siempre ha de comprobarse con certeza que la condición del bucle es la deseada. Cometer un error en la misma es bastante frecuente, y conduce a un error de programación difícil de depurar. En nuestro ejemplo, es probable que el programador cometa una equivocación o dude entre el uso del operador de comparación < y el <=. En este ejemplo, si se hubiese usado este último, el bucle habría iterado una vez más, es decir N+1 veces.

Como ya hemos visto en otros ejemplos, el lenguaje C puede combinar varias operaciones en cada línea de código, de forma que podemos encontrarnos el bucle anterior escrito de la siguiente manera:

i=0; while (i++<N) { <operaciones a realizar>; /*Aqui i vale 1 unidad

mas en cada iteracion */ }

Nótese que el operador elegido para incrementar i, es el de post-incremento, concordando así con el código anterior. Si se hubiera usado el operador de pre-incremento, habríamos cometido un error, puesto que el bucle iteraría una vez menos, desde i=1 hasta i=N-1. Es importante tener en cuenta el comentario que se ha añadido, ya que el uso del operador de post-incremento, conduce a que la variable i se incremente antes de la ejecución de las operaciones del cuerpo del bucle.

Sin embargo, en general en lenguaje C es más usual iterar los bucles empezando por el valor 0 de la variable contador, y llegando hasta el valor N-1. Por tanto, así lo haremos en el resto de ejemplos salvo indicación contraria.

En ocasiones, nos interesará que el contador cuente hacia atrás. Esto no es mayor problema, si la condición del while se cambia para que sea falso en cuanto que el contador rebase el límite inferior de la cuenta y se utilice el operador de decremento en lugar del de incremento.

Page 36: capitulo4

142 ♦ Capítulo 4: Fundamentos de Programación.

Por otra parte, dado que la condición del while puede ser cualquiera, los bucles cuyo número de iteraciones no es constante o está contenido en alguna variable se pueden escribir bastante directamente con bucles while. Sólo habría que poner la condición requerida en el paréntesis del while. Incluso tal condición puede estar compuesta de varias comparaciones unidas por los operadores lógicos Y u O (que en C son: && o ||). De momento analizaremos un ejemplo donde la condición del bucle no es fácil de conocer cuando se escribe el código, sino que depende de la computación que se realiza al ejecutarlo, y, por tanto, no se sabe a

priori cuantas iteraciones ha de realizar el bucle. Se trata de hallar el primer elemento de la serie ∑=

m

k

k1

3 que

supera el valor 10000. La forma más simple de encontrar tal elemento es calcular todos los términos de la serie y preguntar cuando se supera el valor 10000 (condición de finalización del bucle). Si desarrollamos la serie usando la calculadora y anotamos los diferentes términos en una tabla, obtenemos lo que se recoge en la Tabla 4.18.

m 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

m3 1 8 27 64 125 216 343 512 729 1000 1331 1728 2197 2744 3375

∑=

m

k

k1

3 1 9 36 100 225 441 784 1296 2025 3025 4356 6084 8271 11015 14390

Tabla 4.18

La tercera fila se halla fácilmente sabiendo que ∑∑−

==

+=1

1

33

1

3m

k

m

k

mkk . Es decir, para hallar el término m-

ésimo de la serie basta con coger el término anterior y sumarle el valor de m3.

Para calcular los términos de la serie, se van acumulando en una variable (suma) los cubos de los números enteros, representados por la variable k. Recuerde que el operador += permite acumular o incrementar una variable. Inicialmente tenemos que poner a 0 la variable acumuladora suma, y valorar k con 1, para que k vaya luego recorriendo los números enteros. Entonces tal algoritmo podría escribirse con un bucle while así:

k=1; suma=0; while ( suma <= 10000 ) { suma += k*k*k; k++; } printf ("El primer elemento que supera 10000 es el %d\n", k-1);

La solución del ejemplo anterior es el número 14 (compruebe la tabla anterior), luego 1000014

1

3 >∑=k

k .

Obsérvese que la solución se corresponde con k-1, y no con k, puesto que antes de salir del bucle se había ejecutado la sentencia k++;.

Sin embargo, en los bucles que obedecen a esquemas como el de la Figura 4.13, donde la condición de finalización depende de lo que introduzca el usuario, es necesario leer tal número siempre antes de evaluar la condición. Esto quiere decir que el fragmento que lee tal número debe repetirse en dos sitios si se quiere usar un bucle while. Por ejemplo, dicho esquema se escribiría en C como:

Page 37: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 143

#include <math.h> ... ... printf (“Dame un numero (negativo para salir): “); scanf (“%f”, &x); while (x>=0) { printf (“La raiz cuadrada es: %f\n”, sqrt(x) ); printf (“Dame un numero (negativo para salir): “); scanf (“%f”, &x); }

Observemos que antes del bucle se solicita un número, entonces la condición de finalización del mismo pregunta si es negativo. Si es negativo, el bucle acaba, pero si no lo es, se calcula la raíz cuadrada7 y al final del bucle se vuelve a pedir otro número para así volver a empezar. La salida por pantalla de tal programa para los números introducidos 4, 144, 745.98 y –6 (con éste valor se sale del bucle) sería:

Dame numero (negativo para salir): 4 La raiz cuadrada es: 2 Dame numero (negativo para salir): 144 La raiz cuadrada es: 12 Dame numero (negativo para salir): 745.98 La raiz cuadrada es: 27.31263443 Dame numero (negativo para salir): -6

4.4.2 El bucle do...while

El hecho de que sean frecuentes los bucles donde ha de ejecutarse cierto trozo de código antes de preguntar por la condición de finalización, hace conveniente definir algún tipo de bucle que lo permita, de forma que la primera iteración siempre se ejecute, y la condición se sitúe al final. A este tipo de bucle se le denomina do...while, que en inglés significa “haz … mientras”. La sintaxis sería como se muestra a continuación:

do { <sentencia>; } while (condicion);

Aunque las llaves que rodean la sentencia no son necesarias, se aconseja ponerlas para evitar confusiones con el bucle while y por legibilidad del código. También aquí la sentencia puede ser una única línea de código o un bloque encerrado entre llaves. La sentencia sería el “cuerpo” del bucle, y puede contener algún tipo de actualización necesaria para evaluar la condición del bucle.

El esquema del bucle do...while obedecerá a uno como el mostrado en la Figura 4.14.

7 Para hallar la raíz cuadrada se está usando la función sqrt (del inglés square root). Para usar esta función debe incluirse al

principio del fichero de código la línea #include <math.h>

Page 38: capitulo4

144 ♦ Capítulo 4: Fundamentos de Programación.

SI

Operaciones

NO

¿Continuar?

Continúa

Inicilización

Figura 4.14

Al igual que en bucle while, la condición para continuar iterando puede ser cualquiera, incluso compuesta por varias comparaciones.

La utilidad del do...while es evidente cuando el bucle que buscamos tiene que iterar al menos siempre una vez. Un caso típico de esta situación es cuando se le pide al usuario que introduzca un valor, pero antes de continuar con la ejecución debe comprobarse que el valor introducido está dentro de cierto rango. Por ejemplo, si se desea introducir un valor en la variable entera m que pertenezca al intervalo [m_min, m_max], un código elegante usando este bucle sería:

do { printf (“Introduzca un numero entre %d y %d:”, m_min,m_max); scanf (“%d”, &m); } while (m < m_min || m >_max);

El bucle anterior daría por pantalla los siguientes mensajes tras la introducción de los números que se especifican:

Introduzca numero entre 1 y 10: 0 Introduzca numero entre 1 y 10: -4 Introduzca numero entre 1 y 10: 293 Introduzca numero entre 1 y 10: 8 (fin del do...while)

Obsérvese como la condición escrita en este ejemplo es la contraria de la querida para la variable m, de manera que el programa no saldría del bucle mientras m no adquiera un valor del rango deseado.

Siguiendo con el ejemplo de la serie ∑=

m

k

k1

3 , pensemos en escribir el bucle que calcula el término m-ésimo

de la misma. Partiendo de una variable acumuladora suma inicializada a cero, tendríamos que iterar m veces, donde m se supone mayor o igual que uno (m=0 no tiene sentido para la serie). El cuerpo del bucle será similar al mostrado en el ejemplo con el bucle while, pero la condición de finalización es sencillamente haber realizado m iteraciones. Utilizando además el ejemplo anterior para introducir un número, cuyo rango es que sea mayor o igual que uno, podríamos escribir algo como:

Page 39: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 145

do { printf (“ ¿Que elemento quiere calcular? : “); scanf (“%d”, &m); } while (m<=0); /* A la salida del bucle anterior seguro que m > 0 */ suma=0; k=0; do { k++; suma += k*k*k; } while ( k < m ); printf ("El termino %d es: %d\n", m, suma);

Un ejemplo de la ejecución del bucle anterior sería:

¿Que elemento quiere calcular? : -2 ¿Que elemento quiere calcular? : 3 El termino 3 es: 36 También hemos de señalar que se puede convertir un bucle while (como los de la sección 4.4.1) en otro

do...while. Esto implica que nuestro bucle siempre realizaría al menos una iteración, con lo cual el programa seguramente no sería correcto (no lo sería evidentemente si la condición de finalización del while implica que no debe realizarse ninguna iteración). Por ejemplo, si se quiere iterar desde 0 hasta N-1 el cuerpo del bucle, el código podría ser así:

i=0; do { <operaciones >; /* Aqui siempre se itera una vez */ } while (++i < N);

Obsérvese como se ha usado el operador de pre-incremento para la variable contador i. De haber usado el post-incremento se habría cometido el error de iterar una vez más (desde 0 hasta N). También hay que tener en cuenta que la variable i recorre exactamente los valores 0, …, N-1 en el transcurso del bucle.

Por otra parte, un caso típico de uso de los bucles do...while, son los denominados “menús”. En ellos se presenta una serie de opciones (un menú) al usuario que ejecuta nuestro código, y se le pide que introduzca, por ejemplo, un número correspondiente a la opción quiere ejecutar. Tras elegir la opción, el programa debe ejecutar las operaciones correspondientes. Una de las opciones se corresponde con un número que indica que se va a salir del menú, de forma que la condición de finalización del bucle es si el número introducido es el de salida del menú.

Imaginemos un sencillo programa que de momento presenta dos opciones, para calcular las potencias de números enteros:

Calcular el cuadrado del número. Calcular el cubo del número

A éstas le añadimos otra para cambiar el número con el que se trabaja, y por supuesto, otra para salir. De esa forma tenemos un menú de cuatro opciones, que presentaremos al usuario, de momento con sencillos printf. Esto constituye la primera parte del cuerpo del bucle, que siempre se debe iterar una vez para mostrar el menú. Después de eso se pide al usuario que elija una opción, que almacenamos en la variable opc. Y finalmente se trabaja con la opción y el número, realizando las operaciones deseadas. El código podría ser algo así:

Page 40: capitulo4

146 ♦ Capítulo 4: Fundamentos de Programación.

num=0; do { printf ("\n"); printf ("MENUS DE LAS POTENCIAS DE NUMEROS \n"); printf ("1: Introducir nuevo numero.\n"); printf ("2: Hallar el cuadrado.\n"); printf ("3: Hallar el cubo.\n"); printf ("0: Salir del programa.\n"); printf ("\n Elija opcion: "); scanf ("%d", &opc); if (opc == 1) { printf ("Deme un numero: "); scanf ("%d", &num); } if (opc == 2) { printf ("El cuadrado de %d es %d \n", num, num*num); } if (opc == 3) { printf ("El cubo de %d es %d \n", num, num*num*num); } } while ( opc != 0 );

Al ejecutarlo obtendríamos algo como esto:

Page 41: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 147

MENUS DE LAS POTENCIAS DE NUMEROS 1: Introducir nuevo numero. 2: Hallar el cuadrado. 3: Hallar el cubo. 0: Salir del programa. Elija opcion: 1 Deme un numero: 4 MENUS DE LAS POTENCIAS DE NUMEROS 1: Introducir nuevo numero. 2: Hallar el cuadrado. 3: Hallar el cubo. 0: Salir del programa. Elija opcion: 2 El cuadrado de 4 es 16 MENUS DE LAS POTENCIAS DE NUMEROS 1: Introducir nuevo numero. 2: Hallar el cuadrado. 3: Hallar el cubo. 0: Salir del programa. Elija opcion: 3 El cubo de 4 es 64 MENUS DE LAS POTENCIAS DE NUMEROS 1: Introducir nuevo numero. 2: Hallar el cuadrado. 3: Hallar el cubo. 0: Salir del programa. Elija opcion: 0 (fin de la ejecución) Evidentemente la presentación de este menú es bastante pobre (por ejemplo, está repitiendo

continuamente las opciones del menú, en lugar de dejarlas fijas en alguna ventana), y las operaciones de cada opción son muy simples; pero es un buen punto de partida para empezar.

4.4.3 El bucle for

Los bucles se usan a menudo para recorrer una serie de elementos, contar algo, etc. A causa de lo común de estas operaciones dentro de un programa medianamente útil, C incorpora una notación más legible para estos bucles: el bucle for. La palabra inglesa “for” se corresponde con la preposición española “para”. En concreto un bucle for, podría traducirse en pseudocódigo algo como: “para todos los elementos que cumplen cierta condición; realiza la sentencia del cuerpo del bucle, y tras cada iteración ejecuta ciertas actualizaciones”. Un ejemplo de un “para” lo tenemos también en el signo ∀ que se emplea en la notación matemática para formular teoremas. Por ejemplo: ...10:1Ni∈∀ significaría “para todos los números naturales entre 1 y 10, hacer...”. Las palabras que hemos señalado en cursiva son las acciones de que consta un bucle for, y su sintaxis es así:

for (inicializacion; condicion; actualizacion) <sentencia>;

De igual manera que los bucles vistos anteriormente, la sentencia (“cuerpo” del bucle) puede ser una única línea de código o un bloque entero entre llaves.

Page 42: capitulo4

148 ♦ Capítulo 4: Fundamentos de Programación.

La principal cualidad de los bucles tipo for es su legibilidad, ya que en su sintaxis se incluyen la inicialización y la actualización, de forma que para bucles simples no es necesario que en la sentencia se incluyan las actualizaciones necesaria para evaluar la condición del bucle, como pasaba en los bucles anteriores. También las operaciones de inicialización permiten evitar que el bucle vaya precedido de forma poco elegante de las mismas.

Recordar cuál es cuál dentro de los tres campos separados por ‘;’ del bucle for puede no ser fácil al principio; por eso aconsejamos que se recuerde el caso más típico de bucle que itera N veces (como siempre desde 0 hasta N-1, pues es lo habitual en lenguaje C), y que escrito con for y usando i como variable contadora, queda como:

for (i=0; i<N; i++) { <operaciones a realizar>; }

Aquí se distingue claramente cómo inicializamos8 la variable i a 0, establecemos como condición de salida que se alcance el valor N, y tras cada iteración se incrementa i. Nótese la elegancia de este código respecto del escrito con un bucle while, puesto que todo lo relativo a la iteración de las operaciones queda encerrado en los paréntesis del for.

El diagrama de flujo del for obedecerá a lo mostrado en la Figura 4.15, donde se ha dibujado también el diagrama de flujo referente al ejemplo de iterar N veces.

Viene

SI Operaciones a realizar (cuerpo

del bucle)

NO

¿La condición es cierta?

Actualización

Inicialización

Continúa

Viene

SI Operaciones a realizar (cuerpo

del bucle)

NO

i<N

i++

i=0

Continúa

Figura 4.15

Obsérvese en este diagrama de flujo que la acción de actualización puede ser indistintamente la sentencia i++; como la ++i; puesto que se ejecutan independientemente del cuerpo del bucle: después del cuerpo y antes de volver a evaluar la condición. También es importante recordar que cuando el bucle se acaba, la actualización se ha realizado una vez más, precisamente para que la condición no sea cierta. Esto significa, por ejemplo en el bucle anterior, que la variable i vale N al salir del bucle y no N-1, aunque la última iteración hubiese sido la i=N-1. O lo que es lo mismo, en este bucle tipo, N coincide con el número de veces que se ha ejecutado el bucle.

Al igual que en los otros bucles, la condición para continuar iterando puede ser cualquiera, incluso compuesta por varias comparaciones. Y además las acciones de inicialización y actualización también pueden ser cualesquiera sentencias válidas de C, o incluso dejarse vacías. Por todo eso el bucle for es muy flexible y

8 En un bucle for se puede inicializar más de una variable. Para ello basta separar las sentencias de asignación por comas, de

esta forma: for (i=0, j=0 ; ...... ; .......)

Page 43: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 149

denso, permitiendo representar muy diversos bucles en sólo una línea. Por ejemplo, con éste se puede escribir un bucle while de forma obvia:

for (; condicion;) <sentencia>;

equivaldría a:

while (condicion) <sentencia>;

puesto que se han dejado vacíos las acciones de inicialización y actualización (nótese que los ‘;’ del bucle for deben permanecer para no cometer un error sintáctico). Como ejercicio, trate de poner en la forma más compacta posible con un bucle for, el ejemplo visto para el bucle while que trataba de hallar el primer elemento de la

serie ∑=

m

k

k1

3 que superase el valor 10000 (y por supuesto, ¡la solución debe ser la misma!).

De forma análoga, las acciones de inicialización y actualización pueden ser cualesquiera, permitiendo muchas posibilidades. A continuación se muestran algunas posibilidades, en las que se la sentencia elegida sirve para comprobar el valor que va tomando la variable índice del bucle:

a) Bucle que muestra los primeros N números pares (el caso para los impares sería análogo). Obsérvese como i va “saltando” de dos en dos:

for (i=2; i<=2*N; i=i+2) /* la actualizacion podria haber sido i+=2 */ printf (“Valor de i: %d\n”, i);

Valor de i: 2 Valor de i: 4 Valor de i: 6 ... ...

b) Bucle que itera N veces usando i como variable contadora, pero contando hacia atrás desde N-1 hasta

0, inclusive:

for (i=N-1; i>=0; i--) printf (“Valor de i: %d\n”, i);

Valor de i: 4 (suponemos que N vale 5 al entrar en el for) Valor de i: 3 Valor de i: 2 Valor de i: 1 Valor de i: 0

c) Bucle donde la variable contador no se incrementa sino que se multiplica como una progresión

geométrica de razón r (aquí x es variable real, float o double)

Page 44: capitulo4

150 ♦ Capítulo 4: Fundamentos de Programación.

for (x=r; x<=r*r*r*r*r*r; x=x*r) printf (“Valor de x: %f\n”, x);

Valor de i: 0.5 (suponemos que r vale 0.5 al entrar en el for) Valor de i: 0.25 Valor de i: 0.125 Valor de i: 0.0625 Valor de i: 0.03125 Valor de i: 0,00048828125

d) Se pueden usar como contadores variables de tipo char. Por ejemplo, el siguiente código muestra el

alfabeto del código ASCII:

char c; for (c=’A’; c<=’Z’; c++) printf (“%c”, c);

El resultado que se mostraría por pantalla sería el siguiente:

ABCDEFGHIJKLMNOPQRSTUVWXYZ

4.4.4 Anidamiento de bucles

Hasta ahora hemos tratado con bucles que contenían un cuerpo muy simple. Pero, en muchos casos las operaciones del cuerpo suelen ser más complejas y contener varias sentencias. Por ejemplo, un caso muy típico de bucle es aquel que recorre una lista de elementos y busca dentro de ella un patrón o circunstancia, bien contando cuántas ocurrencias de este patrón se producen o bien saliendo del bucle a la primera ocurrencia. Este tipo de bucles es muy común cuando se trabaja con listas o tablas, como veremos más adelante. Para mostrar un ejemplo, el siguiente bucle cuenta, dentro del rango de números comprendido entre a y b, cuántos números son múltiplos de 5 pero no lo son de 7:

ocurrencia=0; for (i=a; i<=b; i++) { if ( !(i%5) && (i%7) ) ocurrencia++; } printf (“Hay %d numeros multiplos de 5 pero no de 7\n“, ocurrencia);

Sin embargo, es mucho más habitual incluso, que en el cuerpo de un bucle exista otro bucle, es decir haya dos bucles anidados, uno externo y otro interno. En informática, esto es muy común cuando se trabaja con algún dato o estructura de datos de dos dimensiones, por ejemplo, matrices. Aunque las matrices las estudiaremos en otro apartado, podemos aquí mostrar un ejemplo donde son necesarios bucles anidados. Imaginemos que tenemos una matriz A de orden m×n, cuyos elementos9 son aij (0<=i<m, 0<=j<n), y necesitamos “recorrer” todos los elementos de la matriz, por ejemplo para buscar cuántos elementos con valor cero contiene, para multiplicar la matriz por un número λ constante (operación λA), etc. Entonces habría que mover i, j por todo su rango de valores; cada vez que i variase se cambiaría de fila, y cada vez que lo hiciera j se cambiaría de columna. De forma que para recorrer la matriz tendríamos que variar por cada fila (es decir, para cada i) todas las columnas (es decir, mover j por todo su rango). En concreto, deberíamos escribir:

9 Como hemos comentado en otros ejemplos, en lenguaje C es más conveniente numerar los índices desde 0 a m-1.

Page 45: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 151

for (i=0; i<m; i++) /*BUCLE EXTERNO, RECORRE FILAS*/ { for (j=0; j<n; j++)/*BUCLE INTERNO, RECORRE COLUMNAS */ { <operaciones con el elemento aij> /* Aqui se cambia de columna*/ } /*FIN DEL BUCLE INTERNO*/ /* Aqui se cambia de fila*/ } /*FIN DEL BUCLE EXTERNO*/

En la ejecución de los bucles anteriores, las variables i, j van tomando los valores de todos los índices de los elementos de la matriz en el orden que se muestra en la Tabla 4.19.

Para i=0 recorre j como: j=0 j=1 ... j=n-1

Para i=1 recorre j como: j=0 j=1 ... j=n-1

… … … ... …

Para i=m-1 recorre j como: j=0 j=1 ... j=n-1

Tabla 4.19

Es decir, se recorre la matriz por filas, y dentro de cada fila se van procesando sus elementos. Nótese que la elección del bucle externo como recorrido por filas no es obligatoria; la matriz puede elegir una columna (j), y recorrer todas las filas (i):

for (j=0; j<n; j++) /*BUCLE EXTERNO, RECORRE COLUMNAS*/ { for (i=0; i<m; i++) /*BUCLE INTERNO, RECORRE FILAS*/ { <operaciones con el elemento aij> /* Aqui se cambia de fila*/ } /*FIN DEL BUCLE INTERNO*/ /* Aqui se cambia de columna */ } /*FIN DEL BUCLE EXTERNO*/

Ahora, las variables i, j irán tomando los valores de todos los índices de los elementos de la matriz en el orden dado por la Tabla 4.20.

Para j=0 recorre i como: i=0 i=1 ... i=m-1

Para j=1 recorre i como: i=0 i=1 ... i=m-1

… … … ... …

Para j=n-1 recorre i como: i=0 i=1 ... i=m-1

Tabla 4.20

Extendiendo este ejemplo a más dimensiones, es evidente que para recorrer un dato de tres dimensiones, necesitaríamos escribir tres bucles anidados, uno por dimensión, y así sucesivamente para más dimensiones. En tal caso, es importante tener en cuenta cómo se dispara el número de iteraciones que el programa va a ejecutar. Por ejemplo, supongamos que trabajamos con bucles de 1000 iteraciones. Poner un bucle y otro después (sin anidación) supondría evidentemente ejecutar 2000 iteraciones. Pero si tuviésemos que anidar dos bucles,

Page 46: capitulo4

152 ♦ Capítulo 4: Fundamentos de Programación.

entonces el número de iteraciones sería 1000×1000=1.000.000 (es decir, dos dimensiones). Y si nuestros datos fueran de tres dimensiones, entonces habría que ejecutar 1000×1000×1000 = 1.000.000.000 de iteraciones, con lo cual nuestro programa podría tardar bastante tiempo en ejecutarse completamente. Pruebe a poner tres bucles anidados, aunque sólo lleve el incremento de una variable double como operación, y vaya aumentando el número de iteraciones hasta que la ejecución tarde un tiempo apreciable; lo más probable es que a partir de la décima parte de la frecuencia de reloj de su procesador, el tiempo de ejecución sea apreciable (aunque depende de muchos factores como el compilador, el tipo de computador, el sistema operativo, etc.). Por ejemplo, para una CPU a 2 GHz, 200 millones de iteraciones tardarían seguramente algunos segundos.

La necesidad de anidación de bucles también surge cuando se estudia un fenómeno físico de dos dimensiones. Aquí, lo más probable es que haya que estudiar los efectos de tal fenómeno en un plano, es decir, variando sus coordenadas X e Y. Imaginemos que queremos representar el potencial eléctrico de dos cargas q1 y q2, colocadas en (x1, y1) y (x2, y2) respectivamente, en las cercanías del origen de coordenadas. La fórmula del

potencial eléctrico de una carga es bien conocida: rqKV = , donde K vale 9*109[Vm/C], q es la carga en

Coulombios, y r la distancia del punto a la carga. Para realizar tal representación habría que calcular el potencial de las dos cargas (y sumarlo) en cada punto del plano. Como sabemos que los ordenadores trabajan de forma digital, no se puede representar una función de forma continua sino discreta, por tanto, hay que “trocear” el plano en pequeñas casillas, en cada una de las cuales se calculará el potencial. Este proceso se suele llamar “discretización”, y es lo habitual cuando se estudian fenómenos físicos en el plano o en el espacio. El número de casillas en que se divide el plano puede elegirse muy pequeño, pero entonces la discretización es muy tosca o imprecisa, y habría un salto muy grande de potencial entre una casilla y sus contiguas (cada casilla sería un trozo muy grande del plano). Por el contrario, si el número de casillas es elevado (casillas muy pequeñas), tendremos una gran precisión, pero, como vimos antes, el número de iteraciones se dispara (un cálculo por cada casilla). En el límite del número de casillas tendiendo a infinito, tendremos casillas infinitesimales y la precisión sería perfecta (¡pero tendríamos que esperar una eternidad para que cualquier computadora realizara el cálculo!).

En el siguiente fragmento de código se ha discretizado un rectángulo de 6.0×4.0 m2 centrado en el origen, y se ha calculado el potencial de dos cargas.

incremento_x=0.2;incremento_y=0.2; for (x=-2.0; x<=2.0; x+=incremento_x) { for (y=-3.0; y<=3.0; y+=incremento_y) { distancia1 = sqrt( (x-x1)*(x-x1) +(y-y1)*(y-y1) ); distancia2 = sqrt( (x-x2)*(x-x2) +(y-y2)*(y-y2) ); potencial = (9.0e+9)*(q1/distanc1 + q2/distanc2); /* Aqui se representaria el potencial */ } }

En el cuerpo del bucle interno habría que representar gráficamente el potencial de las dos cargas. El tratamiento gráfico depende de las librerías gráficas de que dispongamos. Por último, dos aclaraciones: a) se ha elegido un tamaño de cada casilla de incremento_x × incremento_y = 0.2×0.2 m2, suficiente para ver las variaciones del potencial (es una hipérbola que no tiene cambios bruscos); y, b) Si las coordenadas (x,y) coincidieran con la posición de alguna de las cargas, tendríamos una distancia nula, y por tanto una división por cero, obteniendo un error en la ejecución. Habría que evitar de alguna forma que esto ocurriera en nuestro programa definitivo.

El caso de datos en varias dimensiones no es el único que requiere bucles anidados, aunque tal vez sea el más común. En otros muchos cálculos es necesario recurrir a anidación de bucles. Por ejemplo, si queremos iterar algo cuyo cálculo encierra otro bucle, nos aparecerán bucles anidados. Imaginemos que hemos de calcular

el primer número entero de la sucesión n

n

+

11 que se aproxime a su límite, es decir al número

n

n ne

+=

→∞

11 lim en una milésima. Como no sabemos a priori los valores de tal sucesión, aunque sí conocemos

el valor de e, podemos sencillamente iterar aumentando el valor de n de uno en uno hasta que la diferencia entre

Page 47: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 153

el término de la sucesión y su límite e sea menor que 0.001. Pero el cálculo de la n-ésima potencia de un número requiere un bucle de n iteraciones. Por tanto, podríamos escribir algo como:

#define NUMERO_e ( 2.718281828459) n=1; aprox_e=2.0; while ( (NUMERO_e - aprox_e) > 0.001) { n++; aprox_e=1.0; for (i=0; i<n; i++)aprox_e= aprox_e*(1+1/(double)n); } printf ("El entero que se aproxima en 0.001 es %d\n", n); printf (" Y el valor aproximado es %f\n ", aprox_e);

En este ejemplo, el bucle for se encarga de la potenciación n-ésima, y cuando se termina se pregunta en el while si la aproximación del número e se acerca a una milésima.

Una ejecución de este código tendría como resultado:

El entero que se aproxima en 0.001 es 1357 Y el valor aproximado es 2.717283 Como ya dijimos la elección entre for o while es sencillamente por costumbre, y le aconsejamos

que ensaye convirtiendo un tipo de bucle por el otro. Por otra parte, fíjese en que el primer término de la sucesión se calcula fuera del bucle while, de no ser así la primera condición del while carecería de sentido (habría que poner un do…while; inténtelo también).

Por último, vamos a terminar con un ejemplo donde el bucle interno depende del externo, en concreto, el número de iteraciones que realiza el interno depende del externo. Se trata de mostrar las combinaciones de n

elementos tomados de 2 en 2. Sabemos que pueden existir hasta ∑−=

=−

==

1

1

2

2)1(

2 nkn knnC

n combinaciones, y

que se pueden extraer por medio de muchos algoritmos. Uno de los más simples, consiste en coger el primer elemento y añadirle seguidamente todos los demás, obteniendo así n-1 combinaciones. Con esto el primer elemento se descarta ya, y tomamos el segundo, al cual le añadimos los que están por detrás de él, obteniendo ahora n-2 parejas. Con el tercero obtendríamos n-3, y así sucesivamente hasta el penúltimo, que sólo se puede combinar con el último (esta suma demuestra la última igualdad de la ecuación anterior). Ese proceso de extracción de parejas lo podemos describir fácilmente con bucle for: un bucle for externo se va a encargar de recorrer los elementos del primero al penúltimo, mientras que el interno sólo recorrerá desde el elegido por el externo hasta el último (ahí está la dependencia). La combinación se compone del elemento que indica el bucle externo más cada uno de los que está recorriendo el interno. Por ejemplo podríamos escribir algo como:

#define ASCII_a ( 97 ) for (i=0, num_comb=1; i<num_elem-1; i++) { for (j=i+1; j<num_elem; j++) { printf ("COMBINACION %4d: ", num_comb++); printf ("%c", ASCII_a+i); printf ("%c\n", ASCII_a+j); } }

Para num_elem = 4, la ejecución volcaría este resultado en pantalla:

Page 48: capitulo4

154 ♦ Capítulo 4: Fundamentos de Programación.

COMBINACION 1: ab COMBINACION 2: ac COMBINACION 3: ad COMBINACION 4: bc COMBINACION 5: bd COMBINACION 6: cd Nótese que para la representación de las parejas de combinaciones nos hemos apoyado en el hecho de que

los códigos ASCII de las letras son correlativos, siendo el 97 para la ‘a’, el 98 para la ‘b’, etc. De forma que la impresión de los caracteres cuyo ASCII es 97+i y 97+j, implica que la salida del programa serán las parejas: ab, ac, ad, etc. Si el num_elem fuera mayor que el número de letras del abecedario ASCII, nos saldrían los caracteres siguientes a la ‘z’.

4.4.5 Una aclaración: la sentencia break en bucles

Ya vimos que la sentencia break ”rompía” la ejecución saliéndose de la sentencia switch. Para el caso de un bucle el efecto de un break es análogo: el bucle se interrumpe cuando se encuentra con tal sentencia, es decir, se deja de iterar y se continúa ejecutando lo que hubiese después del bucle (si hay llave de cierre ‘}’, detrás de ella). Evidentemente un break dentro de un bucle impide su ejecución, y tal código es seguramente inútil:

for (i=0; i<10; i++) { break; } /* i valdra cero tras el bucle*/

Pero sí es útil cuando hay que dejar de iterar porque ha ocurrido algo en la iteración que estamos ejecutando. Así en el ejemplo primero del apartado 4.4.4 vimos cómo en cada iteración se preguntaba por cierta condición con una sentencia if (en ese ejemplo la condición era que un número fuera múltiplo de 5 pero no de 7). Supongamos ahora que buscamos el primer número múltiplo de 137 dentro del rango [a,b]. Lo normal es que Vd. no conozca ninguna “prueba del 137”, o sea, un algoritmo simple para saber si un número es o no múltiplo de tan extraño número primo (si fuera así, ¡perdón!, Vd. debe ser un matemático eminente). Por lo tanto, para encontrar el primer número múltiplo de 137 hay que acudir a la “fuerza bruta”: probar uno por uno desde a hasta b. En lenguaje C, por ejemplo, escribiríamos:

Page 49: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 155

for (i=a; i<=b; i++) { if ( !(i%137) ) break; } if ( i!=(b+1) ) printf ("El primer multiplo de %d es %d\n", 137, i); else printf ("No hay multiplos de 137 entre %d y %d\n", a, b);

SI.Entonces

i<b+1

NOSI

NO.Entonces

i=b+1

i<=b

i++

i=a

Continúa: discernirentre las dos salidas

!(i%137)

Figura 4.16

Véase que se ha usado la sentencia break para dejar de iterar en cuanto se encuentre un múltiplo de 137. Pero en ese caso, se puede llegar a acabar el bucle por dos motivos: el propio break, o la finalización de todas las iteraciones (ver Figura 4.16). Por lo tanto, hemos de encontrar alguna condición tras el bucle que permita discernir por cuál de las dos salidas acabó el bucle. En este ejemplo (y en la mayoría de los demás que tienen esta estructura), es el valor del índice del bucle el que indica que salida se tomó: si es menor (o distinto) que b+1, entonces el bucle acabó “prematuramente” por el break, y por lo tanto se detectó un múltiplo de 137.

De todas formas, no conviene abusar de la sentencia break, si se quiere mantener una programación estructurada. Por ejemplo, en un bucle con un cuerpo grande la existencia de varios break, hace menos legible el código, ya que habría varias vías de salida del bucle. Por eso, sólo recomendamos su uso en la sentencia switch, o en un único punto por cada bucle cuando su necesidad sea patente. Por ejemplo, puede comprobar como ejercicio que el siguiente bucle produce iguales efectos al anterior, y éste no usa la sentencia break:

for (i=a; i<=b && (i%137); i++) { }

4.5 DISEÑO DESCENDENTE.

Ahora ya conocemos suficientes sentencias del lenguaje C como para escribir un buen programa y no limitarnos a ejemplos cortos. Evidentemente un programa grande ocupará muchas líneas de código. Si miramos por ejemplo el tamaño en Kilo o Megabytes de cualquier programa comercial que tenemos en nuestro ordenador y lo comparamos con el ejecutable de cualquiera de los ejemplos que hemos hecho, nos daremos cuenta de lo largos que éstos deben ser. Un programa comercial no sólo tiene miles y miles de líneas de código, sino que seguramente ha participado en su construcción todo un equipo de programadores. Evidentemente de momento no

Page 50: capitulo4

156 ♦ Capítulo 4: Fundamentos de Programación.

vamos a embarcarnos en la tarea de diseñar un programa gigantesco, pero sí debemos ir ya mostrando las pautas de cómo organizar adecuadamente un programa, para que cuando hubiese que escribir uno grande, no cometamos errores de estructuración y tengamos ya la mente “estructurada” para la programación.

La idea de la estructuración es un tópico en los lenguajes de programación (denominados “estructurados”) pues surge de algo evidente: si un programa tiene muchas páginas de código, éste es “intratable” y cualquiera puede perderse intentando buscar algo en él, y no digamos algún error de programación. Por tanto, hay que dividirlo de alguna forma en fragmentos que sean más fáciles de escribir, analizar, comprobar su correcto funcionamiento y depurar. En el lenguaje C, además de definir sentencias que fueran estructuradas, se buscó que esta fragmentación también lo fuera. Por ejemplo, la idea de los cuerpos de código encerrados entre llaves {}, estructura las sentencias, desde el momento en que tales cuerpos se pueden construir de forma independiente.

La estructuración en C se basa en el empaquetamiento de esos fragmentos en “cajas negras” que puedan ser usadas, escritas, depuradas, etc. de la forma más independiente posible, y una vez que se ha comprobado que son totalmente correctas, olvidarse de cómo están hechas o de qué contienen. Por tanto, el programa grande se puede construir basándose en tales cajas, de la misma forma que un edificio se construye a partir de ladrillos, puertas, tejas, etc. cada una de ellas supuestamente sólida y duradera. Estas cajas se suelen llamar subrutinas, rutinas (porque ejecutan de forma rutinaria su cometido siempre de la misma forma), o procedimientos (porque siguen cierto proceso), y en lenguaje C toman un nombre derivado de las matemáticas: funciones. Cuando al ir ejecutando el código, se encuentra una función, se abandona provisionalmente el código original, se ejecuta entonces la función, y cuando ésta acaba, se continúa con el código, justo detrás de la función que se ejecutó. Se suele decir que se ha “saltado” a la función y que se ha vuelto o retornado de ella.

Al igual que en las matemáticas, las funciones pueden calcular un valor a partir de ciertas variables independientes (y=f(x)), y dar tal valor como resultado (“valor devuelto” por la función), pero en general pueden ejecutar cualquier tipo de código válido en C.

A partir de las funciones se pueden construir los programas de forma descendente, es decir, desde arriba hasta abajo (en inglés, el término es diseño “top-down” que también se escucha en el mundo de la informática). El método descendente consiste en: primero plantearse lo que va a hacer nuestro programa en “líneas generales”, y por tanto, qué partes (funciones) necesitará. Después, en el nivel inferior se desgranan cada una de las partes, y, si aún los fragmentos son grandes o difíciles de analizar y depurar, se subdivide cada parte en otras menores. Y así sucesivamente hasta llegar al nivel inferior donde encontraríamos sentencias de código C exclusivamente.

Como hemos sugerido con el ejemplo arquitectónico, el proceso de estructuración no es algo nuevo de la programación: cualquier obra o proyecto grande empieza por definir las “líneas generales” que se pretenden conseguir, y a partir de ahí va desgranando las partes de que se compondrá la obra. Siguiendo con nuestro ejemplo tendríamos que: para construir un edificio, un arquitecto se plantea primero preguntas cómo: ¿cuántos pisos va a tener el edificio?, ¿cuántas casas y de qué tamaño?, ¿hacia dónde va a orientarse?, etc. Definidas las respuestas digamos que puede ya construir una maqueta del edificio, donde no se verán los detalles particulares de cada piso. Entonces, resueltas las cuestiones generales, se introduce en cada una de las casas y va diseñando cada uno de sus planos por separado. Por último, va entrando en detalles de las casas, como los enchufes, azulejos, etc. que van a tener (ver Figura 4.17).

EDIFICIO: CUESTIONES PLANTA DE UN PISO DETALLE DE UNA HABITACIÓN GENERALES

Figura 4.17

Page 51: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 157

Con este ejemplo en mente podemos enunciar algunas de las ventajas y características de un diseño descendente:

Al igual que lo lógico es que los enchufes o azulejos de todas las casas sean iguales y se diseñen idénticamente, una función puede utilizarse repetidamente en varios puntos del programa, evitando así repeticiones tediosas.

Además si estas funciones son lo suficientemente generales y útiles, se pueden usar en otros programas (concepto generalmente llamado reusabilidad). Tal es el caso de las funciones printf o scanf que venimos usando en todos los ejemplos anteriores. Como ya hemos visto antes, las funciones que se consideren útiles para otros programas se agrupan en librerías.

Si todas las funciones son como cajas negras cuyo contenido no interfiere con las otras funciones, los programas son más fiables, en el sentido de que es más fácil verificar la exactitud de cada función por separado y luego construir un programa como conjunto de funciones ya analizadas. Es importante evitar que la modificación de una función afecte a las otras.

En general, los programas estructurados son más fáciles de depurar. Si existe un error en algún sitio, se puede empezar con la búsqueda del problema por las funciones de arriba, detectar cuál funciona mal y continuar bajando dentro de la incorrecta hasta llegar al error concreto. Por tanto, hay que intentar que el programa se estructure en trozos pequeños fáciles de depurar.

No obstante todas estas ventajas, el tiempo dedicado al diseño puede ser más mayor: hay que plantearse antes de empezar a programar qué va debe hacer exactamente cada función, qué variables necesita y qué valores devuelve. Un cambio de última hora en la especificación de una función obligará seguramente a modificar todas las capas o niveles por encima de la misma.

Además es aconsejable que las funciones sean “robustas” ante fallos en su uso. Por ejemplo, sabemos que la función scanf lee un valor del teclado y lo introduce en una variable. Pero, ¿qué pasa si el usuario introduce por teclado una letra en lugar de un número? La función scanf debe avisar de alguna forma al programador de este posible error en el uso de la función. Más adelante explicaremos cómo se suele hacer esto.

En resumen, un programa debe tener un aspecto en cuanto a su organización similar al de la Figura 4.18, donde cada rectángulo sería una función en C y algunas de las cuales se usan en varios puntos del programa.

Fin

Inicio

SI

NO

Función 1

Función 2

Función 3

Función 2

SI

NO

Función 3

Figura 4.18

En la sección siguiente vamos a empezar precisando la forma de trabajar con funciones en lenguaje C, para ir sucesivamente mostrando ejemplos de diseño descendente en el resto del texto.

Page 52: capitulo4

158 ♦ Capítulo 4: Fundamentos de Programación.

4.5.1 Funciones.

La estructuración de los programas en lenguaje C se basa sobre todo en la fragmentación y el “empaquetamiento” de los programas en funciones. Como hemos dicho antes, es aconsejable que una función sea una “caja negra” que realiza ciertas acciones de forma totalmente independiente, devolviendo si es necesario ciertos valores de salida (lo que devuelve la función) a partir de valores de entrada (llamados parámetros de la función; en algunos otros libros, argumentos). La independencia de las funciones no sólo facilita el análisis y la depuración sino que permite la reusabilidad de las funciones para otros programas.

La forma más comprensible de trabajar con una función de C es similar al concepto matemático: obtener un resultado a partir de ciertos parámetros. Por ejemplo, en matemáticas puedo definir las funciones ‘cuadrado’ y ‘cubo’, de la siguiente manera:

Cuadrado: f1(x)= x2; f1: R→ R

Cubo: f2(x)= x3; f2: R→ R

Y a partir de ahí, trabajar con ellas, con f1 y f2 normalmente. Por ejemplo podría crea una función polinómica f3(x) de la siguiente forma:

Polinomio: f3(x)= f1(3x) - 4f2(x) + 6; f3: R→ R

Incluso el nombre de la variable independiente (parámetro real en lenguaje C) puede cambiarse al definir una nueva función:

Polinomio: f3(z)= f1(3z) - 4f2(z) + 6; f3: R→ R

De forma que cuando usemos f3(z), se estará evaluando el valor:

f3(z)= 9 z2 – 4 z3 + 6

Por tanto, para trabajar con una función en C hay que tener siempre en mente tres pasos:

1. Declaración o prototipo de la función: informa de cómo debe usarse (se correspondería con f2: R→ R en matemáticas). Aquí se indican los parámetros formales de la función.

2. Definición o implementación: informa de qué hace (se correspondería con f2(x)= x3). 3. Llamada: el punto del programa donde se usa (por ejemplo usa f3(z) usa f1() y f2() de la siguiente

manera: f1(3z) - 4f2(z) + 6). Aquí se envían los valores concretos para los parámetros formales, es decir, se fijan los parámetros reales o efectivos.

La sintaxis de estos tres pasos en lenguaje C es similar a la de matemáticas y se recoge en el siguiente ejemplo:

#define PI 3.1415926 main() { double radio, area_circulo; double cuadrado(double); /*DECLARACION*/ … radio = 0.5; area_circulo = PI * cuadrado (radio); /*LLAMADA */ } double cuadrado (double x) /*DEFINICION*/ { double valor; /*variable de la funcion, no se ve fuera*/ valor= x*x; return (valor); }

Obsérvese que se ha construido la función cuadrado() (el nombre puede ser cualquiera válido en C), primero declarándola junto con las variables, y como si fuera una variable más. De hecho podrían haberse declarado todas juntas, así:

Page 53: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 159

double radio, area_circulo, cuadrado(double);

Allí se especifica que la función necesita un parámetro formal de tipo double (no hace falta ponerle nombre como en el ejemplo matemático) y que devolverá como resultado otro double, expresado a la izquierda del nombre de la función. Por tanto, podemos decir que esta función es del tipo double.

Segundo, se invoca o llama para calcular la superficie de un círculo de 0.5m de radio. El parámetro real que se envía entre paréntesis puede ser cualquier expresión válida en C (en realidad sería el valor de tal expresión). Por ejemplo, si la fórmula para calcular el área del círculo se expresara con el diámetro D en lugar de con el radio R (área=πD2/4, D=2R), entonces la llamada podría haber sido: PI*cuadrado(2*radio)/4; y el resultado de multiplicar el radio por 2 se enviaría como valor del parámetro a la función.

Y por último y tercero, se implementa o define qué hace nuestra función. La definición empieza por una cabecera con los parámetros (esta vez ya con nombre) y el resultado, los cuales deben coincidir con la declaración anterior (si no es así el compilador daría un error). Tras la cabecera se pone directamente el cuerpo de la función (cuidado, sin ‘;’ entre ambos), que puede contener cualquier conjunto de sentencias. En nuestro ejemplo, evidentemente el cuerpo es muy simple, y lo importante es no olvidar lo que devuelve o retorna la función, usando la sentencia return (del inglés “devuelve”). Como se comprenderá la sentencia return supone una salida inmediata de la función, con lo cual se podría salir de una función por varios sitios, si ésta incluyera varias de estas sentencias. Pero esta circunstancia reduce la claridad del código, de forma similar a insertar varias sentencias break dentro un bucle (ver 4.4.5). Por tanto, desaconsejamos la existencia de múltiples sentencias return en una función salvo casos excepcionales donde no se pierda claridad (por ejemplo, si la función es muy corta).

Obsérvese también que el parámetro x pasa a jugar el mismo papel que cualquier variable de la función (valor), de forma que el cuerpo de la función podría haberse simplificado sencillamente como:

return (x*x);

Se ha definido otra variable, digamos “auxiliar”, llamada valor, para que vayamos percatándonos de que las variables se declaran dentro de un cuerpo entre llaves {}, de forma que cada una de ellas pertenece a un cuerpo distinto, y en principio sólo puede ser usada y conocida dentro del mismo. De esa forma, las variables valor y x sólo se “pueden ver” dentro de la función, fuera de ella (fuera de sus llaves {}) no existen. Como ya hemos indicado en otras ocasiones, los cuerpos son independientes entre sí, y como explicamos al comienzo de este apartado, en programación estructurada se intenta que las funciones sean como “cajas negras”, de forma que el código y variables que éstas contengan no debe ser visto desde fuera. Esto se explicará con más detalle en la siguiente sección 4.5.2, y de momento sólo nos quedaremos con el concepto de que una variable sólo puede ser usada en el cuerpo donde se declara para mantener la independencia de los mismos.

De momento, es importante tener en cuenta que aunque el parámetro x sólo puede ser visto por la función cuadrado(), éste siempre toma el valor que se envía desde la llamada; en el ejemplo del código toma el valor 0.5. Si hay varias llamadas, el parámetro va tomando los valores de cada llamada:

radio = 0.5; area_circulo1 = PI * cuadrado (2*radio)/4; /*LLAMADA 1*/ area_circulo2 = PI * cuadrado (7.23); /*LLAMADA 2*/ area_circulo3 = PI * cuadrado (1.0/4+2); /*LLAMADA 3*/

En las tres llamadas anteriores, el parámetro x de cuadrado() iría adquiriendo en cada llamada, los valores que resultan de calcular la expresión entre paréntesis de la llamada (1.0, 7.23 y 2.25 respectivamente).

Hay que hacer énfasis aquí en que una función se declara y se llama como si tratara de una variable cualquiera, y por tanto puede jugar el papel de una sentencia o de una variable. Por ejemplo, podemos escribir:

printf (“el cuadrado de %f es %f\n”, radio, cuadrado(radio)); if (radio > cuadrado (0.7)) radio = cuadrado (0.7); x4 = cuadrado(cuadrado (x)); /*la cuarta potencia */ /* etc. etc. etc. */

Es decir, llamar a una función (cuadrado(), aquí) no es nada más que otro valor (de tipo double en este caso).

Page 54: capitulo4

160 ♦ Capítulo 4: Fundamentos de Programación.

Por otro lado, hay que reseñar que en lenguaje C todo se puede considerar una función. Por ejemplo, todos nuestros programas han empezado por main(). Pues bien, esto significa que todo nuestro programa comienza en una función llamada main(), que significa “principal” en inglés. Nunca se siguieron los tres pasos de toda función porque: la declaración está recogida por defecto; la llamada no la hacemos nosotros, sino que es el sistema operativo quien llama a nuestra función main(), cuando se ejecuta nuestro programa; y por último, la definición es lo que escribimos nosotros. Por tanto, en la definición de la función main() deberíamos haber puesto los parámetros y lo que devuelve. Esto es un concepto más avanzado que se relaciona con el sistema operativo que estemos usando y, de momento, no tiene importancia para seguir trabajando10.

Ahora que sabemos que en C cualquier cosa puede ser una función, vamos a ir describiendo algunos ejemplos. La función más simple es aquella donde se pretende ejecutar un trozo de código sin necesidad de parámetros ni de devolución de valores. Para declarar de alguna manera estas funciones y no entrar en confusión con funciones que sí necesitan parámetros, en C se ha definido un tipo de variable vacío o nulo (en inglés “void”). Por tanto, si por motivos de estructuración o clarificación del código queremos que un fragmento esté en una función aparte, podemos declararla de ese tipo void y con parámetros void (es decir, sin parámetros). Esto es habitual en los programas que contienen un menú, donde es normal por motivos de claridad, incluir cada uno de los fragmentos que deben ejecutarse en cada opción en una función de este estilo. Por ejemplo, retomando el ejemplo de la sección 4.4.2, la sentencia switch de aquel menú podría escribirse como:

switch (opc) { case 0: opcion0(); break; case 1: opcion1(); break; case 2: opcion2(); break; case 3: opcion3(); break; }

Nótese que en la llamada a las funciones no hay ningún parámetro, por tanto, hay que declarar antes las funciones así:

void opcion0(void), opcion1(void), opcion2(void), opcion3(void);

Por último, la definición de cada función contendría el fragmento que se quisiera, terminando por la sentencia return, pero sin devolver ningún valor; por ejemplo:

void opcion0(void) { printf (“Ha elegido Vd. la opcion 0\n”); return; } void opcion1(void) { printf (“Esta es la la opcion 1\n”); return; }

En realidad, si no se incluye ninguna sentencia return en una función, el compilador presupone que está al final de la función; sin embargo, el código queda más claro si siempre se incluye tal sentencia, y aconsejamos su uso. También en lenguaje C existen reglas que determinan de qué tipo son una función y sus

10 No haga caso si el compilador le indica en un mensaje que la función main() debe devolver algo

Page 55: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 161

parámetros, cuando no se les da ninguno en su definición o declaración. Sin embargo, tampoco aconsejamos acogerse a estas reglas por defecto, pues el recordarlas implica una dificultad adicional, y porque puede cometerse un error por confusión. Es mucho más prudente declarar y definir siempre todas las funciones para prevenir errores.

En este punto ya podemos construir funciones que suplanten un fragmento de código con el objeto de evitar repeticiones tediosas de tal fragmento, e incluso para ganar en claridad. Aquí tenemos un ejemplo que sería mucho menos claro (y más difícil de escribir) si no usáramos funciones. La función construida se corresponde perfectamente con el concepto matemático de función (tal como se explicó al comienzo de esta sección). Se trata de hallar una raíz o cero, aproximando al menos en una milésima, de cierta función func_dos_tramos(x), sabiendo que es creciente a partir de x=-4, y de valor negativo en tal punto. La función en este caso tiene dos tramos:

−>=+−−<−−

=2 si ,6

2 si ,43)(__ 23

3

xxxxxxxtramosdosfunc

Aunque existen formas muy rápidas y sofisticadas de encontrar la solución de una ecuación, es decir, el corte con el cero, la forma quizás más simple para nuestro ejemplo es buscar en qué valor de x la función cambia de signo. Así que podemos ir calculando de milésima en milésima el valor de func_dos_tramos(x) hasta que sea positivo, por ejemplo con un bucle while que itere mientras la función sea negativa y deje de iterar cuando cambie de signo:

main() { double x, func_dos_tramos(double); x=-4.0; while (func_dos_tramos(x) <= 0 ) { x+=0.001; } printf ("La solucion aproximada es %f\n", x); } double func_dos_tramos(double x) { if (x<-2) return (x.*x.*x - 3*x –4); else return (x*x*x - x*x + 6); }

La solución aproximada es -1.537. Obsérvese que el uso de dos sentencias return en la función no lleva a confusión, puesto que se corresponde con la definición matemática. Por otra parte, se deja como ejercicio evaluar las complicaciones surgirían en el código si lo escribiéramos sin usar función alguna.

Como estamos viendo, en lenguaje C pueden construirse funciones de cualquier tipo y que usen cualquier tipo de parámetro, por ejemplo: void f1(int); int f2(void); float f3(int); etc.

Por otro lado, al igual que en matemáticas existen funciones de varias variables (por ejemplo z=f(x,y): R2→ R), en C se pueden construir funciones con varios parámetros. Siguiendo con nuestro ejemplo de la función cuadrado, podemos construir una función más genérica (y, por tanto, con más posibilidades de ser usada en otras partes de nuestro programa o en otros programas): la potenciación de cualquier grado. Para elevar cualquier número a cualquier exponente entero, necesitamos enviar esos dos parámetros y la función potenciacion() nos devolverá el resultado. Como Vd. comprenderá los parámetros no tienen por qué ser del mismo tipo, en nuestro caso el exponente será entero. Por tanto la definición y la llamada serán:

double potenciacion (double, unsigned int); /*DECLARACION*/ … z = potenciacion (x, 2); /*LLAMADA, cuadrado de x */ y = potenciacion (x, 4); /*LLAMADA, cuarta potencia */

Pero, ¡esto parece una contradicción: no hemos definido aún el contenido de la función, y ya la estamos usando! No hay peligro: en nuestro planteamiento de diseño descendente, eso son detalles secundarios; es más importante que la declaración y la utilidad de nuestra función sean correctas. La definición concreta de las

Page 56: capitulo4

162 ♦ Capítulo 4: Fundamentos de Programación.

funciones se hace en los últimos pasos del diseño de nuestro programa. Por ejemplo, podríamos escribir una definición como:

double potenciacion (double base, unsigned int exponente) { unsigned int i; double resultado=1.0; for (i=0; i<exponente; i++) resultado = resultado * base; return (resultado); }

Volvamos aquí a insistir en que es muy importante que la función sea “robusta”, es decir, funcione para cualesquiera valores de los rangos de los parámetros. Por ejemplo, con una declaración de exponente sin signo nos aseguramos que no tenga sentido que nuestra función reciba un número negativo como exponente. Si quisiéramos construir una potenciación que admitiera exponentes negativos, la definición de la función sería más

compleja (observe que para calcular esto, hemos de hacer: kk

xx 1

=− ). Y aunque fuera más genérica, habría que

comprobar que fuera útil para nuestros intereses y no fuera demasiado ambiciosa (definir funciones muy genéricas está muy bien, pero conduce a un mayor esfuerzo y tiempo de programación, y el código resultante es más largo y probablemente más lento).

Siguiendo con la robustez, compruebe que si el exponente fuera nulo, la función devuelve 1.0 como resultado, lo cual matemáticamente es correcto.

Un error de codificación muy usual es omitir alguno de los tipos de los parámetros en una definición. Por ejemplo, supongamos la declaración de la siguiente función:

int func(int, int, int, float);

Entonces la cabecera de su definición produciría un error de compilación (del estilo “se esperaba un paréntesis”) si fuera así:

int func(int param1, param2, param3, float param4) { … }

La definición correcta debe declarar el tipo de todos sus parámetros, para que así concuerde con su declaración (donde aparece repetida la palabra clave int tres veces). Así:

int func(int param1, int param2, int param3, float param4) { … }

De todas maneras, antes de ponerse a construir una función muy genérica, consulte las librerías de su compilador: si se trata de alguna operación usual en algún campo de la ciencia o técnica, lo más probable es que ya exista. Así nos ahorraremos tiempo de programación. En este caso seguramente exista en las librerías una función11 pow() que calcule en general xy; con x, y ∈ R. Pero si usamos o llamamos una función que no es nuestra, faltarían dos de los pasos antes explicados. La solución a estos pasos es:

1. Declaración: La encontraremos en algún fichero de cabecera, de extensión .h. En este caso tendríamos que poner: #include <math.h>. La declaración suele ser12: double pow(double x, double y);

11 La palabra pow proviene de “power”, “potencia” en inglés. 12 Como se mencionó antes el nombre de los parámetros es opcional en la declaración.

Page 57: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 163

2. Definición: Está en alguna librería del compilador. Algunos compiladores con entorno de ventanas buscan en las librerías por defecto a la hora de enlazar (en inglés “link”, que a veces se traduce por “linkar”). En otros, generalmente los de comando de línea como los que incorporan los S.O. UNIX y Linux, es necesario añadir algún tipo de opción (en inglés “flag”) al compilar, por ejemplo: cc –lm fichero.c. La opción o flag –lm indica que se busque en la librería matemática.

Ya vimos al final de la sección 4.4.1, otro ejemplo con funciones matemáticas. Allí se tuvo que recurrir a repetir dos veces un fragmento de código por la forma del bucle. Creando una función que englobe el fragmento repetido tendríamos más fácilmente ese código:

#include <math.h> … float x, leer_numero(void); … while ( (x=leer_numero()) >=0 ) { printf (“La raiz cuadrada es: %f\n”, sqrt(x) ); } … float leer_numero(void) /*DEFINICION*/ { float num; printf (“Dame numero (negativo para salir) “); scanf (“%f”, &num); return num; }

Hemos de aclarar que en la condición del bucle while se han condensado varias sentencias: hay una llamada a la función leer_numero(), una asignación a x de lo devuelto por la función, y finalmente una condición >=.

Al igual que las funciones sqrt() o pow(), ahora podemos caer en la cuenta que venimos usando funciones de ciertas librerías, y sin haber visto la declaración explícitamente. Nos referimos a las funciones de entrada/salida, tales como printf() o scanf(). Su declaración está en el enigmático fichero stdio.h que venimos incluyendo en casi todos nuestros programas. El nombre stdio.h procede del inglés “standard input/output”, entrada/salida estándar. La definición se encuentra seguramente en las librerías estándar del compilador y no hace falta indicar nada a la hora de enlazar. De hecho estas funciones se suelen nombrar con la acción que realizan (por ejemplo, “print” significa imprimir en español) más la letra ‘f’(de “function”).

Así, por ejemplo, la llamada que aparece un poco más arriba:

printf (“el cuadrado de %f es %f\n”, radio, cuadrado(radio));

indica que la función ha sido llamada con tres parámetros (separados por comas): el primero es una cadena de letras o caracteres (se estudiarán con más detalle las cadenas de caracteres en el apartado siguiente 4.7), el segundo es de tipo real (float), y el tercero también real (lo que devuelve la función cuadrado).

Por último falta resolver cómo construir funciones que devuelvan varios valores. El símil matemático no es tan común, pero se podría corresponder con una función vectorial. Por ejemplo si queremos definir una función que pase de polares a cartesianas, tendríamos que definir una función p=f(x, y): R2→ R2, donde (x, y) son coordenadas cartesianas y el vector p contendría el radio y el ángulo13 polar. Para construir en C funciones que devuelven varios valores hay que recurrir, bien a devolver un vector, o bien, a mandar los parámetros a la función con “permiso de escritura”, es decir, que la función pueda modificar el valor del parámetro. Estos dos métodos están relacionados con el trabajo con vectores y matrices y por tanto, los estudiamos en el siguiente apartado 4.7.

13 En realidad, el ángulo suele definirse con rango comprendido entre (-π, π] o [0, 2π) y no para todo R. También el radio

podría definirse con rango R+. Sin embargo, como en lenguaje C no existen tipos de variable “angular” o positivo real con esos rangos, vamos a suponer que se devuelven valores reales.

Page 58: capitulo4

164 ♦ Capítulo 4: Fundamentos de Programación.

4.5.2 Ámbito de las variables.

En la introducción de este apartado 4.5, vimos que la estructuración en C se basa principalmente en el empaquetamiento del código en “cajas negras” independientes, o sea en funciones cuyo contenido no pueda verse desde fuera. En cuanto al código esta estructuración es obvia: una función f1 llama a otra f2, y entonces se empieza a ejecutar el código de f2, hasta que éste acaba con la sentencia return, devolviendo el valor que sea o simplemente finalizando. Entonces se continúa ejecutando el código de f1 (la función invocadora o llamante) siguiendo con la sentencia posterior a la llamada a f2. De esa forma f1 no necesita ni puede ver lo que ha ejecutado f2, sólo le interesan los valores que f2 ha devuelto. En la Figura 4.19 se ha esquematizado tal situación, rodeando el código de cada función por una caja “impenetrable” para el resto de funciones. La llamada a f2 también se ha rodeado de una cajita para hacer énfasis en este hecho.

Pues bien, esta independencia ha de mantenerse también para las variables, con el objeto de que las funciones sean realmente “cajas negras”. De esa manera, en general las variables (más los parámetros) con que trabaja una función no pueden ser vistas por ninguna otra, y se les suele llamar variables locales, porque pertenecen a una zona o ámbito y fuera de ella no existen. Es decir, su alcance es la función donde están declaradas. En otros contextos se les llama también automáticas, porque se crean y se destruyen automáticamente al ejecutar esa función, o propias (pertenecientes o propias de una función). Así en la Figura 4.19, las variables locales de f1 no podrán ser “vistas” de ninguna forma por f2, ni viceversa. La única “interferencia” entre ambas funciones es que f1 envía ciertos valores para los parámetros y f2 devuelve un resultado (recogido en la variable a de f2).

f1

a = f2 (valor de los parámetros);

más sentencias de f1

f2

declara sus variables (propias

o locales)

sentencias de f2

parámetros de f2

return (valor)

declara variables

Figura 4.19

Lo anterior implica que dos variables locales de dos funciones pueden tener el mismo nombre y no habrá ninguna interferencia ni conflicto entre ellas ya que son variables totalmente distintas. Esto es habitual con las variables índice de los bucles, las cuales suelen ser denominadas i, j, k, etc. en la mayoría de los bucles, y por tanto, su nombre se repite en muchas funciones.

Según lo explicado antes, ¿qué pretende hacer y qué ocurre en el siguiente fragmento de código?:

Page 59: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 165

void f1(void) { int a1; a2 = 3;

… } void f2(int e) { int a2; a1 = 47; …; }

Como se aprecia, f1 intenta dar un valor a la variable a2, que está declarada como local o perteneciente a f2. En f2 se intenta dar un valor a la variable a1, que pertenece a f1. Como hemos dicho antes, esto es absurdo y este programa no se puede ejecutar: se producirían errores en el proceso de compilación, del tipo “Símbolo (o identificador) a1 no declarado (o no definido)” y “Símbolo (o identificador) a2 no declarado (o no definido)”. Una versión correcta del fragmento anterior podría ser:

void f1(void) { int a; a = 47;;

… } void f2(int e) { int a; a = 3; …; }

Obsérvese que hemos bautizado las dos variables con el mismo nombre, ya que al pertenecer a distintas funciones son realmente variables distintas. A cada variable a se le asigna el valor numérico correspondiente. En cuanto a los parámetros de una función, la situación es la misma (y suele ser muy frecuente). Por ejemplo, en el ejemplo de la función func_dos_tramos() de la sección anterior, la variable x de la función main() no es la misma que el parámetro x de la func_dos_tramos(). Por tanto, modificar el parámetro no afectará a la variable del main():

main() { double x, y, func_dos_tramos(double); x=-4.0; printf ("Valor de x antes: %f\n", x); y = func_dos_tramos(x); printf ("Valor de x despues: %f\n", x); } double func_dos_tramos(double x) { x=3.0; /*no afecta a la x del main()*/ return x; }

La impresión antes y después de la llamada nos dará el mismo valor para x, mientras que en y se recoge lo devuelto por la función (3.0).

Page 60: capitulo4

166 ♦ Capítulo 4: Fundamentos de Programación.

El caso anterior es el caso más simple del ámbito o alcance de una variable, cuando el alcance de ésta se limita a la función donde está declarada. Sin embargo, existen otras posibilidades de alcance que son muy útiles en diversos casos, como por ejemplo, variables que son conocidas por todas las funciones. En lenguaje C hay definidas una serie de reglas para establecer el ámbito de una variable a través de los diferentes cuerpos de un programa. De todas ellas, aquí vamos a estudiar las básicas. La regla básica es la vista anteriormente: una variable definida dentro de una función (o sus parámetros), sólo es conocida por tal función; es una variable local. Además la variable es “creada” cada vez que se entra en tal cuerpo y “destruida” completamente al salir de él. De esa forma, cada vez que se llama a una función, las variables locales se vuelven a crear y no “recuerdan” o “guardan” el valor que adquirieron en la llamada anterior. Esto es lo deseable cuando se quiere que cada función se comporte como una “caja negra”.

Esto implica que todas las variables locales de una función deben ser valoradas (inicializadas) antes de ser usadas en la función, puesto que de lo contrario al llamar a la función, no tendrían ningún valor (o contendrían un valor absurdo). Este hecho tiene dos excepciones:

1. Los parámetros son variables locales, pero que se inicializan con el valor que les manda la función invocante o llamante.

2. Una variable local con el modificador ‘static’ (del inglés “estático”), sí conserva el valor de la llamada anterior; de forma que no es destruida cuando se sale de la función. Esto es útil cuando en la función queremos conservar la cuenta de alguna variable.

Veamos lo anterior con un ejemplo. Imaginemos un programa grande para cálculo mercantil, donde por estructuración, el cálculo de estadísticas se ha situado en una función aparte denominada estadistica(). En tal función vamos a llevar la cuenta de las estadísticas por separado, por tanto declaramos algunas de sus variables con el modificador static. Además esta función lleva un parámetro entero que indica la operación que se quiere realizar (la sentencia switch divide en varios casos en función del parámetro caso). El fragmento del código que nos interesa es el siguiente (se ha usado la función leer_numero() anterior):

Page 61: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 167

float x, leer_numero(void), estadistica(float, unsigned int ); … while ( (x=leer_numero()) >=0 ) { estadistica(x, 0); } /*aqui se podrian calcular estadisticas, por ejemplo: */ printf ("La media de los %f numeros introducidos es %f\n", estadistica(x, 1), estadistica(x, 2) ); /* en estas ultimas llamadas, x no sirve para nada*/ } float estadistica(float nuevo_dato, unsigned int caso) { static float cantidad=0.0, suma_total=0.0, suma_cuad=0.0; float aux=1; switch (caso) { case 0: /*nuevo dato */ cantidad++; suma_total+= nuevo_dato; suma_cuad += nuevo_dato*nuevo_dato; /*no hace falta break, return sale de la funcion*/ return (1.0); /* se devuelve cualquier cosa */ case 1: /* devolver cantidad*/ return (cantidad); case 2: /*devolver media*/ return (suma_total/cantidad); // se deja como ejercicio // case 3: /*devolver sumatorio*/ // case 4: /*devolver sumatorio de los cuadrados */ // case 5: /*devolver desviacion estandar*/ } }

En este programa, cada vez que se lee un número no negativo se llama a estadistica(x, 0), con el segundo parámetro igual a cero para que se actualicen las variables de tal función. Esas variables se inicializan a 0.0 en la primera llamada, pero a partir de ahí conservan su valor (en las siguientes llamadas, la inicialización no se ejecutará), de manera que acumulan la cantidad de elementos, su suma y la suma de sus cuadrados. Sin embargo la variable local aux, se inicializará (a 1) en todas las llamadas que se hagan a estadistica(). Posteriormente, fuera del while se llama a estadistica(), con otros valores en el segundo parámetro para recabar valores estadísticos.

Otro tipo muy común de alcance es el de las variables globales. Como su nombre indica, éstas son las opuestas a las locales, es decir, en lugar de pertenecer a una sola función, las variables globales pueden verse en todo el programa. Esto supone una fisura en la metodología de construcción de funciones como cajas negras, pero es muy útil cuando una variable necesita ser usada por muchas funciones, evitando que tal variable tenga que mandarse como parámetro en cualquier llamada a cualquier función. Imaginemos que en el programa anterior de estadísticas, los números introducidos son precios, y el precio total (la suma de los números) debe ser consultada por otras funciones, por ejemplo, por la siguiente:

Page 62: capitulo4

168 ♦ Capítulo 4: Fundamentos de Programación.

#define ALIMENTOS 1 #define MEDICINAS 2 float precio_con_IVA (unsigned int tipo_producto) { float precio_total; switch (tipo_producto) { case ALIMENTOS: precio_total= suma_total + suma_total * 7/100; break; case MEDICINAS: precio_total= suma_total + suma_total * 4/100; break; } return (precio_total); }

Aquí la variable suma_total no ha sido declarada en la función, porque se ha considerado que es global. Sin embargo esta función puede usarla a conveniencia. También habría que suprimir la declaración de suma_total de la función estadistica(), ya que esta variable no va a pertenecer a ninguna función en concreto, sino a todo el programa. La pregunta es ahora: ¿dónde deben declararse las variables globales? En lenguaje C, éstas se declaran fuera de todas las funciones antes del main(), al principio del fichero. No hace falta utilizar ningún modificador, pues queda claro que esas variables no pertenecerán a ninguna función concreta, sino que serán globales. Las variables globales también se pueden inicializar en su declaración, y aconsejamos que, por claridad y para que no se olvide, se haga allí y no en otro punto del programa. Entonces, el ejemplo anterior quedaría, si la variable suma_total se convirtiera en global, así:

#define ALIMENTOS 1 #define MEDICINAS 2 float suma_total=0.0; /*variable global inicializada */ main() { float leer_numero(void), estadistica(float,unsigned int); float x, precio_con_IVA (unsigned int); … while ( (x=leer_numero()) >=0 ) … printf ("La media de los %f numeros es %f\n", estadistica(x, 1), estadistica(x, 2) ); /* ejemplo de llamada */ printf ("El precio con IVA de alimentos seria: %f \n", precio_con_IVA (ALIMENTOS)); } float estadistica(float nuevo_dato, unsigned int caso) { static float cantidad=0.0, suma_cuad=0.0; float aux=1; switch (caso) … }

Lo mismo puede hacerse con las otras variables declaradas como estáticas, si éstas se necesitan usar en muchas funciones.

Sin embargo, no es bueno abusar de las variables globales porque rompen la buena estructuración de un programa, y sólo hay que usarlas cuando una variable necesite estar presente en la mayoría de las funciones. El hecho de que las variables globales rompan la metodología de construcción de funciones totalmente independientes, puede dar lugar a otros errores de programación más difíciles de detectar. Uno de los más comunes consiste en que una variable global posea el mismo nombre que una variable local de alguna función (cuando el programa es grande es difícil recordar todos los nombres de todas las variables). Aunque esto está permitido en lenguaje C (existen unas reglas para determinar cuál de las dos variables se accedería en una

Page 63: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 169

sentencia) no es aconsejable en absoluto, y para evitarlo, lo mejor es denominar a las variables globales con un nombre largo e inequívoco. En el ejemplo anterior, el nombre “suma” tal vez habría sido demasiado corto y podría haber coincidido con alguna otra variable que hiciera de acumulador; por eso se eligió “suma_total”. Algunos programadores añaden incluso la palabra “_global” a toda variable global.

Otro error de programación difícil de detectar cuando se abusa de las variables globales, es la modificación inesperada del valor de alguna de ellas dentro de alguna función. Cuando muchas funciones escriben o modifican una variable global, una llamada a una función, de la cual el programador ha olvidado que escribía sobre una de estas variables globales, conduce a un error de programación difícil de detectar. En general, muchas funciones escribiendo sobre una variable es una situación difícil de controlar, y este tipo de errores denominados efectos laterales se pueden producir por olvido.

Por último, en C pueden declararse variables en un cuerpo (cualquier fragmento de código entre llaves {} de cualquier sentencia). Estas variables serían locales para ese cuerpo y desconocidas fuera de él. Sin embargo, este tipo de declaración de variables de ámbito tan restringido, no suele ser una práctica habitual en la programación, ya que por claridad de codificación se tiende a que las funciones no sean muy largas, y por tanto, todas las variables locales se declaran al principio de las funciones. Además, esto incrementa las posibilidades de que el nombre de una de estas variables pertenecientes a un cuerpo, coincida con el de otra perteneciente a la función, pudiéndose producir entonces un error (en C existen reglas para discernir qué ocurre en este caso; sin embargo, aconsejamos que por claridad no escriba programas donde suceda esto). Pero eso no impide que, como hemos mostrado en los ejemplos anteriores, entre dos cuerpos sin intersección alguna (como el de dos funciones diferentes) haya variables con el mismo nombre. Recuerde que cada función debe ser una “caja negra”, y esto no restará claridad al código.

4.6 VECTORES, MATRICES Y PUNTEROS.

4.6.1 Vector o tabla unidimensional.

Un vector o tabla unidimensional (también denominado con la palabra inglesa array) es una sucesión de valores del mismo tipo que se referencia por un nombre común y que están situadas en posiciones contiguas de memoria. El concepto de vector en lenguaje C es análogo al de matemáticas, es decir, una secuencia de componentes (o elementos) del mismo tipo agrupados bajo un nombre. Así, para construir un vector en el espacio necesitaríamos tres componentes, que se suelen representar usando subíndices, que se empiezan numerando desde el 1: ),,( 321 xxxx =

r. En C la notación que se utiliza para construir y referenciar a las

componentes de un vector es el símbolo corchete ‘[]‘, y los índices se empiezan numerando desde el 0.

Así, la declaración de un vector unidimensional es:

tipo variable_vector[tamanyo];

Donde tipo es cualquier tipo válido de C, variable_vector es el nombre del vector, y tamaño indica el número de elementos del vector (o dimensión del mismo). Tomemos como ejemplo la siguiente declaración:

int vector[10];

La estructura asociada a esta variable es la mostrada en la .

0 1 2 3 4 5 6 7 8 9 índice componentes

Tabla 4.21

Como muestra la Tabla 4.21, los vectores en C empiezan con el 0 como índice del primer elemento; luego para “recorrer” el vector del ejemplo (es decir, acceder a todas sus componentes) tendríamos que ir desde vector[0] a vector[9]. Hemos de hacer énfasis en que los índices se numeran desde el 0, puesto que en la mayoría de lenguajes de alto nivel (Pascal, Basic, Fortran,...) esta numeración empieza en el 1. En la memoria de

Page 64: capitulo4

170 ♦ Capítulo 4: Fundamentos de Programación.

nuestro ordenador, el compilador reservaría una posición de memoria para cada una de las componentes (de tamaño 4 bytes en el caso de un compilador de 32 bits), siendo todas ellas consecutivas. El vector comienza en la dirección de memoria del elemento cuyo índice es 0.

Como en los programas que manejan muchos vectores, la dimensión de estos suele ser la misma, es habitual declarar ésta con una constante, de forma que si fuera necesario cambiar la dimensión de los vectores, sólo habría que modificar el valor de la constante y compilar otra vez (y no cambiar una por una las dimensiones de todos los vectores). De hecho, el ANSI-C no permite declarar vectores con un número de elementos variable. La expresión que aparezca en tamaño debe ser una constante, o una expresión formada por constantes14. Por ejemplo en el siguiente programa se trabaja con vectores de dimensión 2, tanto en la función main() como en otra_funcion():

#define DIMENSION 2 main() { double vector1[DIMENSION]; double vector2[DIMENSION], vector3[DIMENSION]; ... } void otra_funcion(void) { double v1[DIMENSION], v[DIMENSION], v3[DIMENSION]; ... }

Por tanto, si un cambio en el algoritmo obligara a trabajar con dimensión tres, sólo habría que realizar un cambio en la declaración de la constante.

Como es natural, cada una de las componentes de un vector es sencillamente una variable del tipo del vector, y se puede trabajar normalmente con ella:

vector[0]= vector[2]+ vector[3] * 5; scanf (“%d”, &(vector[1]) ); ...

Sin embargo, en lenguaje C no se puede trabajar con el vector de forma completa, y operaciones vectoriales habituales en matemáticas no pueden tener su analogía directa en C. Por ejemplo, la suma de dos vectores, que en matemáticas se denota así: bax

rrr+= , no puede escribirse en C como:

double x[3],a[3], b[3]; ... x=a+b; /* ¡¡codigo erroneo en lenguaje C!! */

La razón es que el operador + es el de suma de números pero no se puede extender a la suma de vectores15 (idem para la resta, producto escalar, etc.). Además, el nombre de los vectores cobra otro significado en C, que veremos en la sección 4.6.4.

Por tanto, para realizar operaciones con vectores completos, hay que recurrir a bucles que “recorran” todas las componentes de los vectores implicados. El siguiente código nos muestra un ejemplo del recorrido de un vector, asignando a cada elemento un valor igual a dos veces el valor de su índice:

14 Una singular excepción está en los compiladores que comparten el núcleo del GNU C, el cual tiene extensiones del

lenguaje C propietarias. Una de ellas permite declarar vectores usando una variable como valor de tamaño, siempre y cuando esa variable tenga un valor válido antes de ser usada en la declaración. En otro caso, la dimensión del vector queda indeterminada.

15 En C++ sí se permite hacer esto, gracias a la sobrecarga de operadores.

Page 65: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 171

int vector[10], i; for (i=0 ; i < 10 ; i++) vector[i] = i * 2;

El resultado de este ejemplo sería introducir en las componentes del vector los valores {0, 2, 4, 6, 8, 10, 12, 14, 16, 18}.

Para realizar la suma vectorial antes explicada, habría que ejecutar el siguiente código:

double x[3],a[3], b[3] ; int i; for (i=0 ; i < 3 ; i++) x[i] = a[i] + b[i];

Unos vectores que se usan con frecuencia en lenguaje C, y muchas veces de forma trasparente para el programador son los vectores de caracteres, también llamados tiras o cadenas de caracteres. Aunque se puede trabajar con ellos como con otros vectores de otros tipos, su frecuencia ha hecho introducir algunas normas y operadores para escribir más cómodamente código con cadenas de caracteres. Muchas de estas normas ya se han usado sin mencionarlas en apartados anteriores. La primera que explicaremos es la que atiende a los operadores simple y doble comilla (‘ y “). Mientras el primero indica un único carácter (el valor ASCII del mismo), el segundo implica la construcción de toda un vector o cadena de caracteres (en ASCII) que además termina en un valor ASCII especial: el 0x0. Así en el siguiente código, se inicializa la variable c solamente con el ASCII de la letra ‘A’ (0x41), pero la variable cadena2 se inicializa con las letras del texto entre doble comillas más el ASCII 0X0 (como se indica en la ):

char c=’A’; char cadena2[14]=”UNA CADENA”; cadena2[12]=’Z’;

0 1 2 3 4 5 6 7 8 9 10 11 12 13 Índice componentes ‘U’ ‘N’ ‘A’ ‘ ‘ ‘C’ ‘A’ ‘D’ ‘E’ ‘N’ ‘A’ 0x0 ? ’Z’ ?

Tabla 4.22

Obsérvese que tras la última letra de la cadena se introduce el valor 0x0, pero detrás de él, el operador doble comilla no inicializará las últimas componentes (la 11, 12 y 13) de forma que su valor es desconocido. La asignación posterior del elemento 12 introduce la letra ‘Z’ en tal elemento.

Con lo anterior, el ASCII 0x0 sirve para indicar el fin de una cadena de caracteres, y muchas funciones estándar de las librerías de C, consideran que una cadena o vector de caracteres empieza en el índice 0 y termina en aquél que contiene tal valor 0x0. Así, aunque la variable cadena2 puede contener hasta 14 letras, la mayoría de las funciones estándar considerarán que su texto acaba en el elemento de índice 9 (la última ‘A’). Aunque aún no hemos explicado el significado que toma el nombre de un vector o una cadena cuando se usa sin referenciar a ninguna componente (o sea, sin corchetes []), pruebe a imprimir el valor de esta cadena usando, por ejemplo, printf():

printf (cadena2);

En pantalla se imprimirá: UNA CADENA, y la letra Z no aparecerá, es decir, la función printf ha ido imprimiendo caracteres desde el cadena2[0] hasta encontrarse con el valor 0x0. De forma análoga se han construido librerías estándar de tratamiento de cadenas que copian, comparan, etc. cadenas de caracteres.

Sin embargo si el valor de fin de cadena (ASCII 0x0) se eliminara de un vector de caracteres, entonces la cadena no terminaría “nunca”. En tal caso las anteriores funciones que trabajan con cadenas pueden dar resultados imprevisibles, ya que empezarían desde el primer carácter y no pararían hasta encontrar un ASCII 0x0 (lo cual puede que signifique una ejecución con miles y miles de bytes). Por ejemplo, el siguiente fragmento de

Page 66: capitulo4

172 ♦ Capítulo 4: Fundamentos de Programación.

código “malintencionado” puede dar lugar a una impresión en pantalla que no se sabe cuando acabará, puesto que la asignación cadena2[10]=’F’ destruye el carácter ASCII 0x0 (lo sustituye por la letra ‘F’):

char cadena2[14]=”UNA CADENA”; cadena2[10]=’F’; printf (cadena2);

Análogamente, hay que advertir que si se pretende construir una cadena de alguna forma indirecta, (sin usar dobles comillas), ha de incluirse el ASCII 0x0 tras la última letra de la cadena. Por ejemplo, el siguiente bucle escribe las primeras 10 letras del abecedario en una cadena, y además, inserta el ASCII 0x0 tras el bucle para que la cadena se considere correcta.

char cad[15], i; for (i=0 ; i < 10 ; i++) cad[i] = ‘A’ + i; cad[10]= 0; //cad[i]= 0; haria lo mismo pues i=10 tras el for printf (cad);

Por último, hemos de señalar que los compiladores de lenguaje C no suelen comprobar los límites de los vectores, es decir, no introducen ninguna detección de si el índice usado al acceder a una componente está por debajo del 0 o de si excede el valor de la dimensión del vector. Por ejemplo, si definimos un vector de 10 componentes, no tiene sentido acceder a la componente [–1] o a la [10] (¡ojo!, la última es la componente 9) o a la [12], puesto que la declaración de tal vector implica que el compilador reserva en la memoria del ordenador solamente 10 elementos. Así el siguiente código puede dar resultados imprevisibles (generalmente el sistema operativo detectará que este programa accede a componentes no reservadas previamente y abortará la ejecución del programa):

int a=24, b, vector[10]; for (i=-1 ; i < 11 ; i++) vector[i] = i * 2; b = vector[a];

En efecto, en la ejecución del bucle ejemplo anterior se producirán asignaciones a vector[-1] o vector[10], las cuales escribirán en una posición de memoria, la cual, probablemente contenga el valor de otra variable distinta. Este hecho produciría con casi toda seguridad un mal funcionamiento del programa ejecutado (o una interrupción del mismo por parte del sistema operativo, debido a esta “violación” del acceso a la memoria). La lectura de componentes que no existen, como vector[24] en la última sentencia (nótese que a se inicializa a 24), introducirá en b un valor desconocido (seguramente el otra variable del programa), llevando a un funcionamiento erróneo del programa.

Luego debemos de tener en cuenta que es misión del programador, asegurar que el vector se maneja dentro de los límites fijados en su declaración. Aunque esto parezca un inconveniente respecto a otros lenguajes que sí introducen código máquina al compilar para detectar violaciones de acceso, es una ventaja en cuanto que tales instrucciones de detección retrasan la ejecución de los programas, de manera innecesaria si se considera que un programa está bien escrito (y no va a acceder a zonas de memoria no reservadas).

4.6.2 Tabla bidimensional o matriz.

El lenguaje C permite el uso de matrices, es decir, tablas bidimensionales. En general, todo lo que vamos a estudiar en esta sección se puede incluso extrapolar a tablas n-dimensionales (o tensores), aunque en la práctica el uso de vectores con más de 2 dimensiones no es muy común. La declaración de una matriz o tabla bidimensional es:

Page 67: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 173

tipo variable_matriz[N][M];

Donde N y M son el número de filas y de columnas respectivamente (la dimensión de la matriz). Se ha escrito la dimensión con letras mayúsculas, ya que deben ser constantes, y al igual que con vectores se suelen definir con constantes, por ejemplo:

#define N 4 //numero de filas de las matrices que voy a //declarar #define M 5 //numero de columnas de las matrices que voy a declarar main() { double matriz1[N][M], matriz2[N][M]; int matriz_entera[N][M]; ... }

Al igual que con vectores, las matrices se numeran empezando por el índice 0, con lo cual el elemento superior izquierdo es el [0][0] y inferior derecho es el [N-1][M-1]. En la Tabla 4.23 se muestra cual sería la forma y los elementos de una matriz a[4][5], de tamaño 4×5. Véase que el primer elemento de la tabla bidimensional es el a[0][0], el siguiente sería el a[0][1], y así, hasta llegar al elemento a[3][4].

Índice columnas Índice filas

0 1 2 3 4

0 a[0][0] a[0][1] a[0][2] ... A[0][4] 1 a[1][0] ... ... ... ... 2 ... ... 3 a[3][0] ... ... ... A[3][4]

Tabla 4.23

Por otra parte, en lenguaje C las matrices se almacenan en memoria “por filas”, es decir, los elementos de la fila primera (de índice 0) van consecutivos y cuando acaba el último de la primera fila empieza a almacenarse el primero de la segunda, y así sucesivamente hasta llegar a la última fila. La Tabla 4.24 muestra como estaría almacenada en la memoria la matriz anterior a[4][5]. Hay que aclarar que algunos lenguajes de programación (FORTRAN) utilizan el convenio contrario, es decir, almacenan las matrices por columnas.

Fila 0 Fila 1 Fila 2 Fila 3

Memoria (direcciones crecientes): a[0][0] a[0][1] a[0][2] ... a[0][4] a[1][0] ... a[1][4] a[2][0] ... ... a[3][4]

Tabla 4.24

Aunque se puede trabajar con elementos por separado de una matriz, lo habitual es hacer operaciones matriciales con todos los elementos en conjunto. Al igual que no existen operadores vectoriales en lenguaje C, no existen tampoco operadores matriciales, de manera que tales operaciones hay que realizarlas con bucles. Como ya vimos en el apartado 4.5, la anidación de bucles permite el recorrido completo de una tabla bidimensional o matriz, y tal recorrido puede hacerse por filas o por columnas. A continuación se muestra un sencillo algoritmo que nos permite recorrer matrices por filas completamente (ya que el bucle interno actúa sobre los índices de las columnas): En primer lugar se valoran las matrices b y c con ciertos números y seguidamente se realiza la operación matricial suma (en matemáticas se define un operador suma matricial con el mismo símbolo ‘+’, de forma que este algoritmo sería equivalente a la suma de matrices A=B+C).

Page 68: capitulo4

174 ♦ Capítulo 4: Fundamentos de Programación.

int a[4][5], b[4][5], c[4][5], i, j; // se valoran las matrices origen A y B con ciertos numeros. for (i = 0 ; i < 4 ; i++) for (j = 0 ; j < 5 ; j++) { b[i][j] = i; c[i][j] = (i * j) + j; } // A continuacion se realiza la suma matricial for (i = 0 ; i < 4 ; i++) for (j = 0 ; j < 5 ; j++) a[i][j] = b[i][j] + c[i][j] ;

Además del recorrido simple por filas o por columnas, pueden existir otros bucles anidados que mezclen recorridos por filas con recorridos con columnas. Esto ocurre, por ejemplo, en el producto matricial (en matemáticas A=B×C), ya que en tal operación es necesario que una fila de B se multiplique por una columna de C (en los ejercicios de este apartado se estudia esta operación).

Hay que tener en cuenta que, por un lado, la declaración de matrices implica una reserva o consumo de memoria que puede ser importante, y por otro, que el uso posterior de las mismas en bucles anidados puede suponer un tiempo de ejecución considerable. Por ejemplo si se pretende trabajar con una matriz donde se almacena un elemento tipo double por cada píxel de la pantalla de alta resolución, esto supone declarar una matriz de dimensión [1024][768], es decir, un consumo de 8×1024×768=6,291,456 bytes, o sea, más de 6 MB. Posteriormente cada vez que se ejecute un bucle anidado para recorrer tal matriz, ¡se ejecutarán 1024×768=786,432 iteraciones!

Por último y como explicamos antes, se puede extrapolar todo lo anterior a tablas n-dimensionales. Así, el siguiente ejemplo declara un tensor de dimensión 4×5×6 e inicializa todos sus elementos a cero usando tres bucles anidados:

double tensor[4][5][6]; int i, j, k; for (i = 0 ; i < 4 ; i++) for (j = 0 ; j < 5 ; j++) for (k = 0 ; k < 6 ; k++) tensor[i][j][k] = 0;

4.6.3 Definición de puntero.

Los punteros en C son básicos para una programación avanzada y más eficiente. Aunque en este texto no vamos a poder usar algoritmos con punteros en toda su potencialidad, al menos no queremos dejar sin explicar el concepto básico y algunas de las utilidades que sin ellos no podrían realizarse, como por ejemplo:

Que una función modifique o escriba en los parámetros que se le envía. Pasar a una función un vector (o matriz) completo como parámetro.

Para entender el concepto de puntero, hay que recordar algunos conceptos de los lenguajes de bajo nivel; los que se usan en los procesadores (CPU). Recordemos que precisamente el lenguaje C es un lenguaje de alto nivel con algunos aspectos de bajo nivel, de ahí su potencia y popularidad. En concreto, hay que repasar el concepto de dirección y dato de memoria que estudiamos en el capítulo 2. Recordemos que, al final, tras todas las traducciones necesarias para que nuestro código C se ejecute, la CPU no va a trabajar con variables abstractas, sino con direcciones de acceso a memoria (en cada una de las cuales habrá un dato). Así la siguiente asignación:

a = b ;

se traducirá en código máquina, por instrucciones simples que se comportarán de forma parecida a las dos siguientes pseudoinstrucciones máquina:

Page 69: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 175

CARGA REGISTRO, DIR (b) ESCRIBE DIR (a), REGISTRO

Es decir, se accede primero a memoria para leer el dato de la dirección de la variable b, que se almacena en un registro de la CPU, y después se accede a memoria para escribir el dato de tal registro en la dirección de la variable a.

Este sencillo ejemplo es extensible para todas las variables que hemos estado usando, ya sean double, vectores, matrices, cadenas, etc. Por eso, en general todas las variables no son más que direcciones de memoria, y es el compilador el que realiza el “trabajo sucio” de asignar a cada variable una la dirección de memoria que considere más oportuna. Pues bien, un puntero es una variable cuyo valor es precisamente una dirección de memoria. Esa es precisamente la clave por la cual un programa escrito usando punteros suele ser más rápido (eficiente) que otro que no los usa: usar punteros, es decir, direcciones, evita muchas instrucciones que el compilador necesita introducir a veces para calcular las direcciones de las variables.

Pero, claro está, si se inventaron los compiladores para evitar el arduo trabajo de conocer o traducir las variables a sus direcciones, no tendría sentido tener que trabajar en un lenguaje de alto nivel como C con direcciones. Por eso, en realidad, las direcciones que van a contener los punteros se van a usar, pero no tienen por qué conocerse nunca. Eso sí, el puntero debe tener una dirección útil, es decir, la dirección de otra variable de memoria. Para comprender mejor este concepto, en la Tabla 4.25 se han representado los bytes de memoria, cuyas direcciones son la 0xF000F100, 0xF000F101, etc.(es decir, direcciones de 32 bits, como es habitual en los ordenadores actuales). Supongamos que una variable a tipo char (es decir, ocupa un byte) contiene la letra ‘W’, y va a ser colocada por el compilador en la dirección 0xF000F107. Por otro lado, una variable puntero p es colocada por el compilador en la dirección 0xF000F100. Si queremos que el puntero contenga la dirección de a, entonces a partir del byte de la dirección 0xF000F100 debe estar escrito el valor de la dirección de a: 0xF000F107. Este valor ocupa 4 bytes, que serán: 0xF0, 0x00, 0xF1, 0x07, los cuales se observan en la .

Dirección en memoria (bytes)

Dato en memoria

Variable abstracta

0xF000F100 0xF0 0xF000F101 0x00 0xF000F102 0xF1 0xF000F103 0x07

Variable puntero p

0xF000F104 0xF000F105 0xF000F106 0xF000F107 ‘W’ Variable a 0xF000F108

Tabla 4.25

Se dice que el valor de un puntero “apunta” a una variable (en el ejemplo anterior, “p apunta a a”, de ahí que la flecha de la Tabla 4.25 salga de p y apunte a la variable a). También se suele expresar como que “el contenido de p es a”. Notemos finalmente que el tamaño de un puntero depende del tamaño de una dirección, en nuestro caso 32 bits, pero no de lo que apunta el puntero. Es decir, si la variable puntero p apuntase a otra variable x tipo double, aunque x tendría un tamaño de 8 bytes, el puntero seguiría ocupando 4 bytes, es decir 32 bits de dirección.

La manera formal de declarar en C un puntero es la siguiente:

tipo *nombre_variable

Donde tipo es cualquier tipo válido de C y nombre_variable es el nombre de la variable puntero. El símbolo asterisco indicará al compilador que dicha variable es un puntero.

Page 70: capitulo4

176 ♦ Capítulo 4: Fundamentos de Programación.

Existen dos operadores especiales unarios (sólo necesitan un operando) para el manejo de los punteros16: & y *. El operador & devuelve la dirección de memoria de una variable, de ahí que se llame operador de dirección. El operador *, llamado por el contrario de “indirección”, devuelve el valor de la variable a la que apunta. En el ejemplo anterior si se quiere que “p apunte a a”, ha de hacerse: p=&a, es decir, la dirección de a la introducimos en p. Tras esto, si se quiere saber “el contenido de p” puede recurrirse a: *p, que nos daría el valor de a, o sea ‘W’. Es más, desde el momento en que se hace que p apunte a a, *p y a no es sólo que tengan el mismo valor, sino que son la misma cosa, a todos los efectos.

Sigamos viendo con otro ejemplo cual sería, intuitivamente, el funcionamiento de estos operadores. Supongamos que tenemos cuatro variables definidas, las dos primeras como variables enteras, a las cuales llamaremos a y b, y las otras, punteros a variable entera: p1 y p2. En general, es una buena costumbre para empezar a trabajar con punteros, nombrarlos empezando por la letra ‘p’, para distinguir claramente las variables que son punteros de las que son datos. La manera de declarar estas variables en C sería:

int a, b; int *p1, *p2;

O bien, todo junto:

int a, b, *p1, *p2;

Supongamos que el compilador ha ubicado las variables en memoria como muestra la Tabla 4.26 (tenga en cuenta que un entero ocupa 32 bits, 4 bytes, lo mismo que un puntero, supuestas las direcciones de 32 bits). Tras la declaración de las cuatro variables, ninguna tiene un valor definido: a y b contendrán un valor indefinido y los punteros p1 y p2, no apuntarán a ningún sitio. La Tabla 4.26 muestra esta situación.

Dirección en memoria (palabras)

Dato en memoria

Nombre variable

0xF000F100 Valor indefinido a 0xF000F104 Valor indefinido b 0xF000F108 dirección indefinida p1 0xF000F10B dirección indefinida p2

Tabla 4.26

Para empezar a trabajar con las variables, asignamos un valor a cada una:

a = 3; p1 = &a; p2 = &b; b = (*p1) + 5;

El resultado que obtendremos es el que muestra la Tabla 4.27.

16 Desgraciadamente, el símbolo para estos operadores coincide con el de otros (*: producto, &: AND, /*: comentarios), y ha

de tenerse mucho cuidado con no confundirlos.

Page 71: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 177

Dirección en memoria (palabras)

Dato en memoria

Nombre variable

0xF000F100 3 a 0xF000F104 8 b 0xF000F108 0xF000F100 p1 0xF000F10B 0xF000F104 p2

Tabla 4.27

¿Qué ha ocurrido? Evidentemente, a contiene un 3, p1 contiene la dirección de a (0xF000F100), y p2 la de b (0xF000F104), debido a las tres primeras líneas de código. Pero la cuarta línea ha valorado la variable b con “lo que apunta p1” más 5. Como “lo que apunta p1” es a, y a vale 3, el resultado es que b tendrá un 8.

Obsérvese que, aunque en la Tabla 4.27 se muestran las direcciones para aclarar los conceptos, éstas no van a aparecer nunca en el código C, tal como dijimos antes. Por otro lado, conviene resaltar para fijar ideas que la dirección de cualquier variable es siempre constante (es decir, &a,&b son constantes), mientras que el contenido de un puntero (*p1, *p2) es variable.

Pero ¿y si hacemos después lo siguiente:?

(*p2) = (*p1) + 100;

Aquí hemos valorado de forma “indirecta” a la variable b, puesto que p2 apunta a b, y la expresión (*p2) a la izquierda de la asignación, conduce a asignar un valor a b. Por tanto, lo anterior es lo mismo que escribir:

b = a + 100;

Naturalmente la última línea que hemos escrito es más sencilla y comprensible que la penúltima, y por tanto, esta forma de usar los punteros no es realmente útil, pero es la mejor manera de comprenderlos, para usarlos luego cuando realmente sea interesante. A partir de ahora evitaremos el uso de paréntesis en los accesos a los contenidos de los punteros como *p2, puesto que el operador de indirección * tiene una precedencia o prioridad muy alta (ver apartado 4.2) y no se requieren ni son usuales tales paréntesis.

Aclaremos a continuación que varios punteros pueden apuntar a la misma variable, de forma que la modificación del “contenido de uno de los punteros”, implicaría la modificación automática del contenido del resto de punteros (y, por supuesto, de la variable). Esta característica es la que nos permitirá poder modificar los parámetros de llamada de una función, y que esta modificación permanezca cuando salgamos de dicha función (esto se estudiará en la sección 4.6.4). Fijémonos en el siguiente trozo de código:

int a=130; int *p1, *p2, *p3; p1 = &a; p2 = p1; //p2=&a haria lo mismo p3 = p2; //p3=&a haria lo mismo La Tabla 4.28 muestra la memoria, suponiendo que las variables empiezan a colocarse a partir de la

dirección de memoria 0xF000F100.

Page 72: capitulo4

178 ♦ Capítulo 4: Fundamentos de Programación.

Dirección en memoria (palabras)

Dato en memoria

Nombre variable

0xF000F100 130 a 0xF000F104 0xF000F100 p1 0xF000F108 0xF000F100 p2 0xF000F10B 0xF000F100 p3

Tabla 4.28

Por lo tanto, cualquier cambio en el valor de a, o del contenido de los punteros, implicaría un cambio en todas las variables. Así, *p3 = 50, es idéntico que a=50 o *p1=50, y en cualquier caso automáticamente tendremos que: a=*p1=*p2=*p3=50.

Por último en este apartado hemos de explicar un “puntero especial” que siempre existe cuando se declara un vector (o matriz): se trata el propio nombre del vector. En efecto, la declaración de un vector como:

double vect[100];

Implica que, mientras que cada una de las componentes vect[0], vect[1], etc. es un double, la expresión vect sería un puntero a double, que, como es evidente apunta al propio vector. En concreto la expresión vect no es una variable, sino simplemente la dirección (una constante) del vector. De esa forma, tiene sentido modificar un puntero declarado como tal pero no intentar modificar la dirección de vect:

double vect[100], d; double *pd1, *pd2; pd1 = vect; // pd1 tambien apunta ahora al vector pd2 = &d; // modificacion del puntero pd2 vect = pd2; // error de compilacion, vect es una constante

El uso del nombre de un vector como dirección o puntero constante, es fundamental para el paso de vectores como parámetros, lo cual estudiaremos a continuación.

4.6.4 Paso de parámetros por referencia y de vectores a funciones.

Como ya se indicó en el apartado anterior una de las grandes utilidades del uso de punteros es el hecho de que una función pueda modificar o escribir en los parámetros que se le envía. Hasta ahora, las funciones recibían parámetros pero sólo consultaban sus valores para realizar cálculos u operaciones internas. Es decir, pasábamos los parámetros por valor. Con los punteros, ahora además, podrán alterar las variables a las cuales hacen referencia esos argumentos de tipo puntero. Por eso se dice que se “pasan los parámetros por referencia” (en lugar de “por valor”). Veamos un ejemplo de esto en el siguiente problema:

Page 73: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 179

void main(void) { int numero, *p_numero; void anular (int *); ... numero = 5; p_numero = &numero; ... // La variable numero contiene un 5 anular (p_numero); // Llamada a funcion // La variable numero contiene un 0 ... } void anular (int *puntero) { ... (*puntero) = 0; }

En el ejemplo anterior se tiene un programa principal en el que se define un entero y un puntero a entero. También existe una función llamada anular(), que tiene un puntero a entero como parámetro. Esta función lo que hace es poner a 0 la variable entera apuntada por el puntero. Inicialmente la variable numero tiene un 5. A la función anular() se la pasa el puntero p_numero que apunta a numero. En el cuerpo de la función el contenido de la variable numero es modificado de forma indirecta, gracias al puntero p_numero, y después de la llamada a la función, numero contiene un 0. La llamada a la función también podría haberse hecho de la siguiente manera siendo el resultado el mismo:

anular (&numero);

En esta última llamada, se dice que se pasa la variable numero por referencia, ya que se envía la dirección de numero, que se recogerá en puntero, de forma que *puntero modifica el valor de numero.

El paso de parámetros por referencia es casi imperativo, cuando una función ha de devolver varios valores, puesto que en lenguaje C una función sólo puede devolver (usando la sentencia return) un valor. Para ello tendrá que cambiar el contenido de varios de sus parámetros. Por ejemplo si se quiere construir una función que devuelva tres números: la inversa, el cuadrado y el cubo de un número; habrá que declarar varios parámetros por referencia para que en ellos se almacenen esos números. Por ejemplo, se definiría así:

void calculos (float, float *, float *, float *);

Y así se usaría:

Page 74: capitulo4

180 ♦ Capítulo 4: Fundamentos de Programación.

void main(void) { float numero, inv, cuad, cubo; void calculos (float, float *, float *, float *); scanf (“%f”, &numero); /* Las variables inv, cuad, cubo no contienen

ningun valor valido antes de la llamada */ calculos (numero, &inv, &cuad, &cubo); // Llamada a funcion /* Tras la llamada, las variables inv, cuad, cubo

contendran lo requerido */ }

void calculos (float y, float *p_1, float *p2, float *p3); {

*p_1 = 1.0/y; *p2= y*y; *p3 = (*p2)* y; }

Observe finalmente que en la llamada a la función scanf() que venimos haciendo desde casi el principio de este capítulo, se está enviando la variable por referencia, para que precisamente la función scanf() pueda modificar tal variable (escribir en ella lo leído por teclado).

El paso de parámetros a funciones mediante punteros no se limita tan solo a los tipos de datos simples. Hasta el momento, hemos trabajado con funciones que tenían como parámetros variables simples pero nunca vectores o matrices. Esto se debía a que C no podemos pasar un vector completo como parámetro de una función. Sin embargo, si podemos pasar un puntero que apunta a un vector. Para ello bastará con pasar como parámetro el nombre del vector sin índices, como se acaba de explicar en la sección precedente. Veamos un ejemplo:

void main(void) { void funcion1(int *); //declaracion int a[10], b[50]; ... // A continuacion paso como parametro el nombre de los vectores: funcion1 ( a ); //llamada 1; a[0] y a[1] valdran lo mismo tras esto. funcion1 ( b ); //llamada 2; b[0] y b[1] valdran lo mismo tras esto. }

La función no devuelve ningún valor (void) y contiene un parámetro que va a ser un puntero a enteros. La función deberá estar definida por ejemplo así:

void funcion1(int *pvector) //definicion /*esta funcion iguala el primer elemento del vector pasado como parametro al segundo */ { /*el puntero pvector (variable local de funcion1) adquirira en

la llamada 1 la direccion del vector a de la funcion main(), pero en la llamada 2 la direccion de b */

pvector [0] = pvector [1] ; }

Como vemos, dentro de la función se puede usar el vector pvector[] como otro vector más, teniendo siempre en mente que en la llamada 1, se pasó el vector a[] y por tanto el uso de pvector[0], y de

Page 75: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 181

pvector[0] se refieren a a[0], a[1], mientras que en la segunda llamada pvector[0], y de pvector[0] se referirán a b[0], b[1].

El uso del puntero pvector como un vector es más evidente si la definición y la declaración de la función se hacen de la siguiente manera, también válidas:

void main(void) { void funcion1(int []); //declaracion ... } void funcion1(int pvector[]) { pvector [0] = pvector [1] ; }

¿Qué está ocurriendo realmente al ejecutar esta función que usa el puntero pvector? Pues que la función main() pasa como valor del parámetro la dirección del comienzo de un vector (ya sea a[] o b[]), que es recogida por la funcion1() en su parámetro pvector, De esa forma, el vector con que trabaja funcion1() (o sea, pvector[0], pvector[1]) es idéntico al vector a[] del main() en la primera llamada (como si en main() se escribiera a[0]=a[1]), e idéntico a b[] en la segunda llamada (como si en main() se escribiera b[0]=b[1]).

En este tipo de acciones con vectores pasados por parámetro, hemos de advertir, tal como hicimos al final de la sección 4.6.1, que el compilador no va a comprobar los límites de los vectores. Es decir, no se introduce en el ejecutable ningún mecanismo de detección de si el índice usado en la función está por debajo del 0 o de si excede el valor de la dimensión del vector. Por ejemplo, si la funcion1() hubiera usado en la primera llamada cualquiera de los elementos pvector[-1], pvector[10], o en la segunda llamada pvector[-1] o pvector[50], (lo que significaría el acceso a los elementos a[-1], a[10], b[-1], b[50], respectivamente) entonces el código daría resultados imprevisibles. En efecto, los accesos a las componentes [-1] o a las que sobrepasan la dimensión de los vectores a[] y b[], leerán o escribirán en posiciones de memoria que no están reservada en tales vectores, las cuales, probablemente contengan el valor de otras variables diferentes. Este hecho produciría con casi toda seguridad un mal funcionamiento del programa ejecutado (o una interrupción del mismo por parte del sistema operativo, debido a esta “violación” del acceso a la memoria, si éste detecta que el programa ha accedido a componentes no reservadas previamente).

Luego debemos tener en cuenta que es misión del programador asegurar que el vector pasado como parámetro a una función (puntero pvector) se maneja dentro de los límites fijados en la declaración de los vectores originales (a, b). Como ya dijimos, aunque esto parezca un inconveniente respecto a otros lenguajes que sí introducen instrucciones de detección de violaciones de acceso, es una ventaja en cuanto que la ausencia de tales instrucciones de detección acelerará la ejecución de los programas.

Lo habitual cuando una función ha de manejar un puntero que apuntará a vectores de diferente número de elementos es enviar otro parámetro que informe del número de elementos que contiene el vector original (a, b del main() en nuestro caso). Como ejemplo de esto último vamos a escribir y utilizar la función anula() que pone a cero todas las componentes de un vector cualquiera. A esta función le pasaremos como parámetro tanto el puntero al vector como el número de elementos del mismo. Dentro de la misma se realizará un bucle desde la componente 0 a la última, para que ponga a cero los elementos, es decir:

void anula (int pvector[], int num_elem) { int i; for (i=0 ; i< num_elem ; i++) pvector[i] = 0; return ; }

Ahora lo importante es tener cuidado en el uso de esta función, es decir que el parámetro que pasa el número de elementos, nunca exceda la dimensión del vector original. Por ejemplo, para anular las componentes de a[] ha de pasarse 10, y para anular las de b[] debemos pasar un 50:

Page 76: capitulo4

182 ♦ Capítulo 4: Fundamentos de Programación.

void main(void) { void anula (int *, int); //declaracion int a[10], b[50]; ... // A continuacion paso el nombre de los vectores y su // numero de elem. anula ( a, 10 ); //llamada 1; anula las 10 componentes de //a[] anula ( b, 50 ); //llamada 2; anula las 50 componentes de //b[] }

Como hemos visto antes, un error grave ocurrirá si en el uso de la función anula() se comete un error y se envía un número de elementos mayor del que disponen los vectores. Por ejemplo, si nos equivocamos y alteramos los números 10 y 50, el resultado de la primera llamada podría ser catastrófico:

void main(void) { void anula (int *, int); //declaracion int a[10], b[50]; ... anula ( a, 50 ); //llamada 1; ERROR GRAVE: ACCESO A //COMPONENTES NO RESERVADAS PARA a[] anula ( b, 10 ); //llamada 2; solo anula 10 componentes //de b[] }

Notemos finalmente que nuestra función anula() es genérica y puede anular un número de componentes inferior a la dimensión total de los vectores (bastaría con indicarlo en le segundo parámetro, como en anula(b, 10), que sólo anulará 10 elementos de b[]).

Por último, podemos trabajar de forma similar con paso de matrices a funciones. Cuando el vector a pasar es bidimensional tendremos que tener cuidado con la definición de la función, puesto que el compilador debe conocer exactamente, cual es el tamaño de la segunda dimensión. Vemos un ejemplo.

void main(void) { int b[6][5]; void funcion2(int [][5]); //la segunda dimension hay que //darsela ... funcion2 ( b ); // llamada: paso la matriz como puntero ... }

La función deberá estar definida con el tamaño de la segunda dimensión:

Page 77: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 183

void funcion2(int pmatriz[][5]) { pmatriz [0][0]= pmatriz [1][1]; // pmatriz[0][5] no es valido, accede a una columna que // no existe // pmatriz[6][4] no es valido si la matriz original es // b[6][5]. Se accede a una fila que no existe ... }

Función Cometido Devolución char *gets (char *cadena); Lee de teclado una cadena de caracteres y la introduce a

partir de la dirección dada por ‘cadena’ La propia dirección de “cadena”

(normalmente no utilizado)

int puts (char *cadena); Imprime en pantalla la cadena de caracteres que viene a partir de la dirección dada por ‘cadena’

El número de caracteres escritos (normalmente no

utilizado)

int strlen (char *cadena); Devuelve la longitud de la cadena de caracteres que viene a partir de la dirección dada por ‘cadena’

Longitud de la cadena

char *strcat (char *cadena1, char *cadena2);

Concatena la cadena de caracteres dada por ‘cadena1’ con la ‘cadena2’, es decir, añade al final de ‘cadena1’ la

‘cadena2’

La dirección de cadena1 (normalmente no utilizado)

int strcmp (char *cadena1, char *cadena2); Compara alfabéticamente (usando el código ASCII) la cadena de caracteres dada por ‘cadena1’ con la

‘cadena2’

-1 si cadena1<cadena2

0 si cadena1==cadena2

1 si cadena1>cadena2

char *strcpy (char *cadena1, char *cadena2);

Copia la cadena de caracteres dada por ‘cadena2’ en la ‘cadena1’

La dirección de cadena1 (normalmente no utilizado)

Tabla 4.29

Por otra parte, como mencionamos en la sección 4.6.1, se han construido librerías estándar de tratamiento de cadenas que copian, comparan, etc. cadenas de caracteres. En la Tabla 4.29 se explican algunas de las más comunes. Los parámetros de entrada son cadenas de caracteres, que se suponen que acaban en el carácter especial de fin de cadena (ASCII 0x0). Por tanto estas funciones trabajan o iteran con los elementos de las cadenas hasta que se topan con tal carácter de fin de cadena.

Como ejemplo de construcción de una de estas funciones escribimos una posible implementación o definición de una de ellas:

int strlen(char *cad) { int i=0; while (cad[i] != 0) i++; return (i); }

Desde un punto de vista didáctico, se recomienda como ejercicio construir o implementar las funciones anteriores de cadenas. Sin embargo, en la práctica, si se ha de trabajar con cadenas, recomendamos que, cuando sea posible, se usen las funciones de las librerías en vez de las implementaciones “caseras” o particulares, puesto que las funciones de las librerías suelen estar muy optimizadas y ser mucho más rápidas que las traducciones que realizan los compiladores de nuestro código C.

Page 78: capitulo4

184 ♦ Capítulo 4: Fundamentos de Programación.

4.6.5 Inicialización de vectores.

El lenguaje C nos va a permitir inicializar los vectores, en el momento de declararlos. La sintaxis es la siguiente:

tipo variable_vector[tamanyo] = {lista de valores separados por comas};

Un ejemplo podría ser:

int a[5] = {0, 1, 2, 3, 4};

Que introduciría en el vector a[] lo mismo que el siguiente bucle:

for (i=0 , i<5; i++) a[i] = i;

La diferencia está en que el bucle debe ejecutarse, mientras que la inicialización se produce en el momento de declarar las variables.

Cuando el vector tiene más de una dimensión, su inicialización es idéntica, teniendo en cuenta el tamaño de la matriz. Un ejemplo podría ser el siguiente:

int a[3][4] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; //3x4=12 elementos int b[2][3] = { 0, 1, 2, 3, 4, 5 }; //2x3=6 elementos

Nótese que el único separador en el ejemplo anterior son las comas, y que los saltos de línea no son necesarios ponerlos para realizar una correcta inicialización (el compilador no los tiene en cuenta, los ignora). La fila 0 de a contendrá los valores 1, 2, 3, 4; la fila 1 contendrá 5, 6, 7, 8, y el resto para la fila 2.

A pesar de la posibilidad de inicializar matrices y vectores, cuando la dimensión de éstas es grande sería muy engorroso escribir una lista de números separados por comas, y se suele recurrir a bucles. Por ejemplo, observe que el siguiente bucle asigna a la matriz b[] el mismo valor que la inicialización anterior:

for (i=0 , i<2; i++) for (j=0 , j<3; j++) a[i][j] = i*3 + j;

O bien el siguiente bucle:

contador=0; for (i=0 , i<2; i++) for (j=0 , j<3; j++) a[i][j] = contador++;

4.7 LECTURA Y TRATAMIENTO SECUENCIAL DE DATOS.

En este apartado vamos a estudiar cómo podemos almacenar los datos usados o generados por nuestros programas en dispositivos de almacenamiento masivo. Esto nos va ha ser útil tanto, para almacenar información generada por nuestros programas, la cual podremos utilizar o consultar cuando queramos, como para leer datos necesarios para el correcto funcionamiento de dichos programas. Evidentemente, en este apartado vamos a dar las herramientas necesarias para realizar las operaciones anteriormente descritas.

Page 79: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 185

4.7.1 Conceptos básicos.

Los datos que se encuentran almacenados en un dispositivo de almacenamiento masivo suelen estar organizados en archivos o ficheros. Podemos entender un archivo o fichero como un conjunto de informaciones sobre un mismo tema tratado como una unidad de almacenamiento y organizado de forma estructurada para la búsqueda de un dato individual. Así, los archivos pueden contener tanto instrucciones de programas como información creada o usada por un programa. La estructura interna de los ficheros es especificada por el programador, de forma que cuando queramos acceder a un determinado fichero debemos previamente conocer el formato del mismo.

Un aspecto importante de los ficheros es la forma en la cual se accede a ellos, así, en función del modo de acceso podemos clasificar los ficheros en dos tipos:

Dispositivo de acceso directo o aleatorio: en este tipo de dispositivos se puede acceder a la información que hay en su interior en cualquier orden. Si se quiere acceder una determinada unidad de información, no habrá que acceder primero a las unidades de información anteriores a ella. Por lo tanto, podremos obtener la información del dispositivo en el orden que nosotros queramos.

Dispositivo de acceso secuencial: almacenan los datos de manera que sólo pueden ser accedidos en un determinado orden. Por lo tanto, para acceder a una determinada unidad de información, habrá que acceder en secuencia a todas las unidades anteriores a esta. El orden de acceso a la información siempre es el mismo, y no lo determina el programador, sino el orden en el que están grabados los datos dentro del dispositivo.

El ejemplo más típico de dispositivo de acceso secuencial es el de la cinta magnética; la información está grabada a lo largo de la cinta, ya sea de audio o de datos. Imaginemos que es una cinta de audio: para escuchar la canción número cinco de la cinta, forzosamente tendremos que perder un tiempo rebobinando la cinta, para pasar las cuatro primeras canciones, y después escuchar la quinta canción. Otro ejemplo claro es el puerto serie, en el cual los bits de información van llegando en serie, en secuencia.

Siguiendo con el ejemplo, la memoria RAM sería el dispositivo de acceso directo más claro: se puede acceder a cualquier byte de la memoria en cualquier orden y sin esperar un tiempo para ello. Análogamente las canciones de un disco láser (Compact Disc) también son de acceso directo. Para escuchar la quinta canción del disco no tenemos porqué escuchar las cuatro primeras canciones, simplemente se posiciona el láser en el principio de la quinta canción y se escucha. Las canciones se pueden escuchar en el orden que se quiera (el tiempo que se tarda en encontrar una canción es mínimo).

Hoy en día la mayoría de los dispositivos de almacenamiento de los que dispone el programador son de acceso mixto, tales como el disco duro, la disquetera. Estos dispositivos permiten al programador acceder en orden aleatorio a una gran cantidad de datos (desde 1.44MB en el caso de la disquetera hasta centenas de GB en el caso de los discos duros) sin necesidad de que este tenga que preocuparse del orden en el que se grabaron los datos en el dispositivo. No obstante, muchos de estos dispositivos tienen cierta componente secuencial, puesto que cuando el motor de un disco va rotando, va leyendo en orden (en secuencia o en serie) los bytes de información.

Por lo tanto, los dispositivos de acceso directo ponen en una situación más ventajosa al programador, puesto que la programación de dispositivos es más fácil; sin embargo, todavía existen situaciones en las que la única forma de acceso a los datos (o la más conveniente) es la secuencial:

Sigue habiendo dispositivos de acceso secuencial (las cintas de datos siguen existiendo). Los datos nos pueden llegar de forma secuencial en el tiempo, de modo que también hay que leerlos

de forma secuencial (los datos que nos llegan por el puerto serie o la red llegan uno detrás de otro a nuestro puerto de comunicaciones).

Puede que, aunque nuestros datos estén en un dispositivo de acceso aleatorio, tengamos que tratarlos de manera secuencial. Imagínese que tiene un fichero de personal y quiere sacar todas las nóminas del mes: tendrá que recorrer todo el fichero de personal (desde el primero al último, y sin saltarse ninguno) para sacar el impreso de nómina de cada uno de sus asalariados.

Dado que hay motivos de sobra para no olvidar el tratamiento secuencial, los lenguajes de programación tampoco escatiman en funciones para facilitar al programador el acceso a dispositivos de manera secuencial. Los

Page 80: capitulo4

186 ♦ Capítulo 4: Fundamentos de Programación.

lenguajes de programación de alto nivel disponen de un conjunto de funciones genéricas para la E/S, con las que se pueden manejar todos los dispositivos de entrada/salida del sistema de manera secuencial: disqueteras, discos duros, impresora, teclado, pantalla, puerto serie, etc.

4.7.2 Entrada/salida por flujo (streaming).

La E/S secuencial se suele denominar con este término: por flujo (en inglés, “stream”). Las funciones asociadas a ésta, tratan todos los datos como un conjunto de bytes que viajan desde el dispositivo de almacenamiento a la memoria del ordenador o viceversa, a través de un conducto. Los datos viajan de manera secuencial por el conducto, creando así un flujo de datos en éste.

El conducto o flujo es la pieza que nos permite comunicar el dispositivo con la memoria. Por lo tanto, para operar con flujos de E/S se necesita una variable (a la que llamaremos variable-flujo), que representa el conducto con el dispositivo de E/S del que queremos obtener o enviar datos. De este modo, para obtener o enviar datos a un dispositivo, debemos manejar la variable-flujo asociada a este. Los lenguajes de alto nivel disponen de un conjunto de funciones para manejar flujos. Las funciones que se pueden encontrar en la mayoría de los lenguajes de alto nivel asociadas a flujos E/S se comentarán a continuación:

AbrirFlujo

Antes de utilizar una variable-flujo, debemos asociarla al dispositivo en el que queremos realizar las operaciones de lectura o escritura; esto se consigue mediante la función AbrirFlujo. Esta función suele tener dos parámetros: la variable-flujo y el dispositivo que se le quiere asignar. El dispositivo puede ser un disco duro, una disquetera, un puerto serie, un puerto paralelo, el teclado, la pantalla (cuando funciona en modo texto), u otro dispositivo que maneje el sistema operativo. En algunos lenguajes, la función devuelve a su salida un código de error para indicar si se ha podido abrir el dispositivo. Si se ha podido abrir el dispositivo, se inicializa la variable-flujo; en caso contrario, la variable-flujo no contiene un valor válido y no se podrá operar con ella.

CerrarFlujo

Se utiliza para desvincular una variable-flujo de su dispositivo de almacenamiento. Una vez cerrado el flujo, no se podrá operar con la variable-flujo para seguir sacando o introduciendo datos en el dispositivo manejado por esta variable. En algunos lenguajes se devuelve un código de error para indicar si ha habido algún problema al cerrar el flujo.

LeerFlujo

Esta función suele tener los siguientes parámetros: variable_flujo, buffer_entrada y num_bytes. Lee un número de bytes (indicado por el parámetro num_bytes) del dispositivo asociado a la variable variable_flujo, y los almacena en la variable buffer_entrada. En algunos lenguajes la función devuelve a su salida el número de bytes que le ha sido posible extraer del dispositivo, que en un principio no tiene porqué ser igual al número de bytes deseados (por ejemplo, si leemos datos de una cinta magnética, podríamos llegar al final de la cinta antes de leer todos los bytes especificados).

EscribirFlujo

Esta función suele tener los siguientes parámetros: variable_flujo, buffer_salida y num_bytes. Escribe un número de bytes (indicado por el parámetro num_bytes) desde de la variable buffer_salida, en el dispositivo asociado a variable_flujo. La función suele devolver a su salida el número de bytes que se han podido escribir en el dispositivo.

Con estas funciones, la manera de operar con un dispositivo utilizando una variable-flujo es la siguiente:

AbrirFlujo entre la variable_flujo y el dispositivo Realizar las operaciones de escritura y/o lectura en variable_flujo CerrarFlujo de la variable_flujo

Suponga que se quieren mandar tres enteros por el puerto serie de la computadora. Los enteros son de 32 bits es decir, 4 bytes. Este sería un programa en pseudocódigo que envíe los tres enteros.

Page 81: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 187

AbrirFlujo entre variable_flujo_serie y PUERTO SERIE SI el flujo se ha abierto correctamente HACER: EscribirFlujo en variable_flujo_serie 4 bytes de entero1 EscribirFlujo en variable_flujo_serie 4 bytes de entero2 EscribirFlujo en variable_flujo_serie 4 bytes de entero3 CerrarFlujo de variable_flujo_serie SI el flujo no se ha cerrado correctamente HACER: Tratar error de cierre de flujo FIN-SI SINO Tratar error de apertura del flujo FIN-SINO

En el pseudocódigo anterior se muestra como funciona la entrada/salida por flujos y sus instrucciones asociadas. Como se puede ver, la estructura condicional “SI se ha abierto el flujo correctamente HACER” se utiliza para controlar si realmente se ha abierto el dispositivo seleccionado. De lo contrario, no tiene sentido cerrar un flujo que no se ha abierto, ni tampoco escribir en él.

Supongamos que se va a recibir por el puerto serie un paquete de 8 enteros, de los cuales solamente nos interesan los dos últimos enteros para guardarlos en un vector de enteros, y posteriormente procesarlos con alguna función. El siguiente programa en pseudocódigo nos permite procesar los dos últimos enteros del paquete recibido.

AbrirFlujo entre variable_flujo_serie y PUERTO_SERIE SI el flujo se ha abierto correctamente HACER LeerFlujo de variable_flujo_serie 4 bytes y guardarlos en var_basura LeerFlujo de variable_flujo_serie 4 bytes y guardarlos en var_basura LeerFlujo de variable_flujo_serie 4 bytes y guardarlos en var_basura LeerFlujo de variable_flujo_serie 4 bytes y guardarlos en var_basura LeerFlujo de variable_flujo_serie 4 bytes y guardarlos en var_basura LeerFlujo de variable_flujo_serie 4 bytes y guardarlos en var_basura LeerFlujo de variable_flujo_serie 4 bytes y guardarlos en vector[0] LeerFlujo de variable_flujo_serie 4 bytes y guardarlos en vector[1] Procesar vector[0] Procesar vector[1] CerrarFlujo de variable_flujo_serie SI el flujo no se ha cerrado correctamente HACER Tratar error de cierre de variable_flujo_serie FIN-SI SINO Tratar error de apertura de variable_flujo_serie FIN-SINO

En este ejemplo se ve que el código para leer de un flujo es muy parecido al que se utiliza para escribir en él. Sólo varía la operación que hay que realizar sobre el flujo, que esta vez es la operación de lectura. En este ejemplo se muestra una de las características del acceso secuencial del flujo: si queremos procesar los dos últimos enteros de los 8 que vienen del flujo, antes tendremos que leer todos los elementos que los preceden. Por esta razón, los seis primeros elementos se leen pero no se utilizan para nada y sólo se guardan en el vector los dos últimos, que son los que se mandarán a la función de procesamiento.

4.7.3 Entrada/salida por ficheros en C

En algunos dispositivos de almacenamiento se pueden guardar los datos en ficheros, normalmente son dispositivos en los que la información no es volátil (disquete, disco duro, cinta magnética, CDROM, etc.). Un

Page 82: capitulo4

188 ♦ Capítulo 4: Fundamentos de Programación.

fichero es un conjunto de datos relacionados entre sí (por ejemplo bytes, enteros, cadenas de caracteres, etc.), que contienen información sobre un tema común. Por ejemplo, el fichero de empleados de una empresa contendrá toda la información referente a cada uno de los empleados (DNI, nombre, domicilio, etc.).

En muchos casos se puede considerar que un fichero está compuesto de registros17, entendiéndose por registro el conjunto de información acerca de cada uno de los elementos a los que hace referencia el fichero. En el ejemplo anterior, la información de cada empleado constituye un registro; es decir, cada registro estaría formado por el DNI, el nombre y el domicilio de cada empleado.

Un registro, a su vez, puede dividirse en campos, que son la unidad de información más pequeña contenida en el fichero. Siguiendo con el ejemplo de los empleados, los campos de cada registro serían el DNI, el nombre, el domicilio, etc.

Por ejemplo, un fichero que almacene información de los empleados de una empresa puede tener asociado un registro con los campos que se indican en la Tabla 4.30.

Campo Tipo de datos Número de DNI Número entero

Letra DNI Carácter Nombre Cadena de 80 caracteres Apellido1 Cadena de 80 caracteres Apellido2 Cadena de 80 caracteres Domicilio Cadena de 100 caracteres

Tabla 4.30

Otro ejemplo puede ser un fichero dedicado a almacenar las coordenadas de puntos en el espacio, el registro podría tener los campos que se indican en la Tabla 4.31.

Campo Tipo de datos Identificador de punto Número entero

Coordenada X Número real Coordenada Y Número real Coordenada Z Número real

Tabla 4.31

Los registros de estos ejemplos son registros complejos (con varios campos) pero puede que tengamos ficheros con registros más simples. Por ejemplo: un fichero dedicado a guardar números primos tendrá un registro con un único campo, y este será un número entero. También podemos tener ficheros cuyo registro sea un byte. En lenguaje C, en lugar de registro se usa la palabra estructura (“struct”, en inglés). Las estructuras se verán en el siguiente apartado.

El lenguaje C tiene un conjunto de funciones que facilitan la E/S desde las distintas unidades de disco. Dichas funciones llaman al sistema operativo para poder usar un dispositivo de E/S (por ejemplo, fichero), y se adaptan al modo de funcionamiento comentado anteriormente como E/S por flujo. De hecho, estas funciones necesitan una variable-flujo para realizar la entrada/salida. Para manejar un fichero se necesita una variable-flujo, que hay que abrir antes de realizar operaciones de lectura y/o escritura, y cerrar cuando no se vayan a realizar más operaciones sobre el fichero. A grandes rasgos, las funciones que proporciona C para la E/S por ficheros son las siguientes:

17 No confundir este significado del término “registro”, con los registros de la CPU. Por motivos históricos se usa la misma

palabra para ambos conceptos, pero no tienen nada que ver.

Page 83: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 189

fopen(): Asigna un flujo a un fichero en disco. El flujo se puede abrir de varias maneras, permitiendo o no su escritura en el flujo: lectura (“read”, en inglés), escritura (“write”), lectura/escritura (“r/w”), y adición o escritura al final (“append”). Por otro lado, se permite especificar el tipo del fichero (texto o binario, “text” o “binary” en inglés) que será explicado más adelante.

fprintf(), fputs(), fputc(): Se utilizan para escribir datos en ficheros. Estas funciones son similares a las que utiliza C para escribir datos en la pantalla, (printf(), puts() y putchar()), y de hecho sólo se diferencian de estas en que tienen un parámetro adicional, la variable de flujo con la que tienen que operar.

fscanf(), fgets(), fgetc(): Análogamente a lo que ocurre con las funciones de escritura en fichero, también las funciones de lectura en fichero son parecidas a las de entrada por teclado (scanf, gets y getch), con la adición del parámetro extra para la variable de flujo.

fclose(): Estas funciones cierran los flujos E/S, eliminando la relación existente entre la variable de flujo y el fichero de disco.

feof(): se utiliza para saber si se ha llegado al final de un fichero.

El tipo FILE

En C existe un tipo especial de datos llamado FILE (del inglés “fichero”), que se utiliza para manejar ficheros; este contiene toda la información necesaria para que las distintas funciones de E/S por ficheros (que se verán más adelante) puedan realizar su labor. Sin embargo, en el código no se utilizan variables del tipo FILE, sino del tipo puntero a FILE. Por ejemplo, si queremos utilizar una variable para manipular ficheros, a la que llamaremos mi_fichero, debemos declararla de la siguiente forma:

FILE *mi_fichero;

El tipo FILE y los prototipos de todas las funciones de E/S mencionadas anteriormente, están declarados en el fichero “stdio.h”. Para poder utilizar dichas funciones en un programa, se debe incluir este fichero al principio del código mediante la siguiente sentencia:

#include <stdio.h> El acceso a ficheros por medio de E/S por flujo implica un tratamiento secuencial de la información que

hay en el fichero. Esto es posible gracias a que el tipo FILE contiene información acerca de cual fue el último registro al que se accedió (ya fuese para leer o para escribir). Cada vez que se lee o escribe un registro de un fichero, se actualiza posición de lectura o escritura en la variable (de tipo FILE*) asociada al fichero, de esta manera se consigue que operaciones de acceso consecutivas accedan a registros consecutivos del fichero.

Función fopen

Antes de poder utilizar cualquier variable de tipo puntero a FILE, debemos inicializarla para indicarle cuál es el fichero que queremos utilizar en el disco. Esta inicialización se denomina apertura del fichero y se realiza con la función fopen(), cuyo prototipo es el siguiente:

FILE *fopen( char* nombre_fichero, char* modo );

La función fopen() tiene dos parámetros que son cadenas de caracteres: el nombre del fichero que queremos abrir (parámetro nombre_fichero), y el modo de apertura (parámetro modo). Una vez que el fichero especificado ha sido abierto, la función fopen()devuelve un puntero a FILE, que debemos asignar a una variable de este mismo tipo. A partir de ahora, cada vez que queramos leer o escribir en el fichero debemos utilizar dicha variable. Si el fichero no se ha podido abrir debido a algún error, fopen() devuelve el valor NULL. Este valor es una constante que está definida en “stdio.h”, y que representa un puntero nulo.

El nombre del fichero puede estar precedido por la ruta de acceso. Esta nos indica la unidad de disco donde se encuentra el fichero y el conjunto de carpetas (o directorios) en las que hay que entrar para llegar hasta él. Por ejemplo, si para llegar a un fichero llamado “prueba.txt”, tenemos que entrar en la unidad “C:”, después en la carpeta “dir1”, y finalmente en la carpeta “dir2”; entonces, el nombre con la ruta de acceso es “C:\dir1\dir2\prueba.txt”, que en C debemos escribir entre comillas y con las barras (\) duplicadas:

Page 84: capitulo4

190 ♦ Capítulo 4: Fundamentos de Programación.

“C:\\dir1\\dir2\\prueba.txt”

Si no se especifica la ruta de acceso, la función dará por supuesto que se quiere abrir el fichero en la carpeta (o directorio) y unidad de disco donde se ha iniciado el programa.

El modo de apertura indica si el fichero se debe abrir para lectura, escritura, lectura/escritura o adición (escritura al final del fichero). Veamos los diferentes modos:

Modo lectura: sólo se permiten operaciones de lectura en el fichero, no se puede escribir en él, evitando así que modifiquemos accidentalmente su contenido. Después de abrir el fichero, las operaciones de lectura comienzan desde el primer registro. Para poder abrir un fichero en modo lectura, este debe existir; si no es así, se produce un error.

Modo escritura: sólo se permiten operaciones de escritura en el fichero, no se puede leer de él. Si el fichero ya existe al abrirlo en este modo, se borra su contenido; si no existe, se crea un fichero vacío con el nombre especificado.

Modo adición (escritura al final del fichero): permite añadir datos al final del fichero, no se permite leer de él. Si el fichero no existe, se crea; en caso contrario, se abre para escribir, pero todo lo que se escriba se añade al final del fichero, respetando así el contenido anterior.

Modo lectura/escritura: en este modo de apertura se permite escribir y leer en el fichero. Si el fichero existe, se abre sin borrar su contenido; si no existe, se crea un fichero vacío con el nombre especificado. En este modo, las lecturas o escrituras comienzan por el primer registro; por lo tanto, si el fichero existe y escribimos en él después de abrirlo, modificaremos los registros que se encuentren al principio.

En el parámetro modo también se indica el tipo de fichero. Los dos tipos posibles son: texto o binario. Un fichero de texto es una secuencia de caracteres (códigos ASCII) organizadas usualmente en líneas terminadas por un carácter de fin-de-línea (‘\n’). Las funciones de E/S que estudiaremos escriben o leen secuencias de caracteres, y se usan normalmente con ficheros de texto. Por ejemplo, si se utiliza la función fprintf() para escribir el número entero 65535 en un fichero, se escribe la secuencia de caracteres mostrada en la Tabla 4.32.

Byte n Byte n+1 Byte n+2 Byte n+3 Byte n+4 Carácter ‘6’ ‘5’ ‘5’ ‘3’ ‘5’

Decimal 54 53 53 51 53 Valor ASCII Hexadecimal 0x36 0x35 0x35 0x33 0x35

Tabla 4.32

Un fichero binario es una secuencia de bytes que, normalmente, se corresponde con la representación en binario de números enteros (int), números en coma flotante (float, double según formato IEE754), o cualquier otro tipo de variable. Aunque las funciones de E/S que vamos a estudiar también pueden trabajar con ficheros binarios, existen otro tipo de funciones más apropiadas (por ejemplo, fwrite()), que no vamos a estudiar en este curso. Siguiendo con el ejemplo anterior, si escribimos el número entero 65535 (en hexadecimal 0x0000FFFF) utilizando la función fwrite(), se escribirían los bytes indicados en la Tabla 4.33.

Byte n Byte n+1 Byte n+2 Byte n+3

Valor hexaadecimal 0x00 0x00 0xFF 0xFF

Valor binario 0000 0000 0000 0000 1111 1111 1111 1111

Tabla 4.33

En general, los ficheros de texto son ficheros orientados a una salida por la impresora, o para mostrarse directamente en la pantalla, de manera que sean legibles para una persona. Los ficheros binarios se utilizan

Page 85: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 191

cuando la información va a ser leída única y exclusivamente por un programa, de manera que este sabe como interpretar correctamente los datos. Por ejemplo, los ficheros con código C que el programador escribe son ficheros de texto; mientras que el programa ejecutable que se genera al compilar el código es un fichero binario.

En la Tabla 4.34 se muestran los valores permitidos para especificar el modo de apertura.

Modo Significado “rt” Abre un fichero de texto en modo lectura. “wt” Abre un fichero de texto en modo escritura. “at” Abre un fichero de texto en modo adición. “r+t” Abre un fichero de texto en modo lectura/escritura. “rb” Abre un fichero binario en modo lectura. “wb” Abre un fichero binario en modo escritura. “ab” Abre un fichero binario en modo adición. “r+b” Abre un fichero binario en modo lectura/escritura.

Tabla 4.34

Algunos ejemplos del uso de la función fopen() son los siguientes:

var_fichero=fopen(“c:\\archivos\\datos.dat”,”rb”);

La sentencia anterior abre el fichero “datos.dat”, que está en la carpeta “archivos” de la unidad “C:”, como un fichero binario para lectura. Dicho fichero debe de existir ya que de lo contrario no se podría abrir y la función fopen() daría como resultado NULL.

var_fichero=fopen(”datos.x”,”wt”); if (var_fichero == NULL) { /* Error al abrir el fichero */ ... }

En el ejemplo anterior se abre el fichero “datos.x”, que está en la ruta de acceso por defecto de la aplicación, como un fichero de texto para escritura. Si dicho fichero no existe, será creado. Si existe el fichero, se borrará su contenido anterior y se comenzará a escribir en él. Después de ejecutar la función fopen(), se comprueba con la sentencia if si el fichero se ha abierto correctamente.

var_fichero=fopen(“a:\\fichas.txt”,”at”);

Finalmente, la sentencia anterior abre el fichero “fichas.txt” que se encuentra en la unidad “a:” (la disquetera) como un fichero de texto para adición. Si el fichero existe se conservará su contenido original y se comenzará a escribir a partir del último registro (sin borrar este). Si el fichero no existe, se creará un fichero con este nombre y se empezará a escribir en él desde el principio.

Función fprintf

Esta función se comporta exactamente igual que printf(), pero en vez de escribir por pantalla, escribe la información en un fichero. El formato general de la declaración de fprintf() es:

int fprintf( puntero_file, “cadena_formato”, lista_parametros );

donde puntero_file es un puntero a FILE y el resto de parámetros coinciden con los de la función printf. Esta función escribe en el fichero asociado a puntero_file los caracteres que aparecen en cadena_formato, sustituyendo los códigos de control (%d, %f, %c, ...) por los valores correspondientes de la

Page 86: capitulo4

192 ♦ Capítulo 4: Fundamentos de Programación.

lista de parámetros (lista_parametros). Por ejemplo, el siguiente código escribe en el fichero “salida.txt” el texto “3 euros son 499.16 ptas”.

FILE *fich = fopen( “c:\\salida.txt”, “wt” ); fprintf( fich, “%d euros son %f.2 ptas”, 3, 3 * 166.396 );

La función devuelve el número de bytes que se han escrito en el fichero, si no se ha podido escribir nada devuelve el valor especial EOF, que es una constante declarada en “stdio.h”.

Función fputs

La función fputs() nos permite escribir cadenas de caracteres en un fichero. El prototipo de la función es el siguiente:

int fputs( char *cadena, FILE *puntero_file );

Esta función escribe en el fichero asociado al parámetro puntero_file los caracteres contenidos en en el parámetro cadena, que es una cadena de caracteres. La función devuelve a su salida un valor mayor que cero si ha podido escribir los caracteres y en caso contrario devuelve el valor especial EOF. Por ejemplo:

FILE *fich = fopen( “c:\\salida.txt”, “wt” ); fputs( “Esto es una prueba”, fich );

Función fputc

El formato general de la declaración de fputc() es:

fputc( caracter, puntero_file )

Escribe en el fichero asociado al parámetro puntero_file (de tipo FILE*) el carácter indicado en el parámetro caracter. La función devuelve dicho carácter si ha podido escribirlo en el fichero; en caso contrario, devuelve el valor especial EOF. Por ejemplo, la siguiente sentencia escribe el carácter ‘h’ en el fichero asociado a la variable fich (de tipo FILE*). Si la escritura se realiza sin problemas, la función devuelve el carácter ‘h’.

fputc( ‘h’, fich );

Función fscanf

Esta función se comporta exactamente igual que scanf(), pero en vez de leer los datos del teclado, lee la información de un fichero. El formato general de la declaración de fscanf() es:

int fscanf( puntero_file, “cadena_formato”, lista_dir_var )

donde puntero_file es un puntero a FILE, y el resto de parámetros coinciden con los de la función scanf(). Esta función lee del fichero asociado a puntero_file los valores especificados en cadena_formato. El parámetro cadena_formato contiene una serie de códigos de control (%d, %f, %c,...) que le indican a fscanf() que tipo de datos debe leer (int, float, char, ...). La Tabla 4.35 muestra algunos de los códigos de control permitidos.

Código de control Tipo de dato leído %c char %d int %f float %lf double

Tabla 4.35

Page 87: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 193

Cada valor leído del fichero es asignado a las variables cuya dirección se especifica en la lista de direcciones (lista_dir_var). La función devuelve el número de valores que han sido leídos del fichero correctamente y almacenados en las variables. Si la función ha llegado al final del fichero y ha intentado seguir leyendo, devolverá el valor EOF. Por ejemplo, la sentencia lee del fichero asociado a la variable fich (de tipo FILE*) tres números de tipo float (%f), int (%d) y double (%lf) respectivamente, y los asigna a las variables var_f1 (de tipo float), var_i (de tipo int) y var_f (de tipo double) en el orden indicado.

fscanf( fich, “%f%d%lf”, &var_f1, &var_i, &var_f2 );

Cuando fscanf() lee un número (int, float o double) del fichero de entrada, lee y desecha todos los espacios y los caracteres de fin-de-línea hasta encontrar un número. Si en vez de encontrar un número se encuentra un carácter no numérico (una letra o algún símbolo de puntuación) entonces la función termina sin haber leído ningún valor numérico.

Función fgets

El prototipo de fgets() es:

char *fgets( char *cadena, int max, FILE *puntero_file );

Esta función lee una secuencia de caracteres del fichero asociado puntero_file y los almacena en el parámetro cadena. La función termina cuando ha leído max-1 caracteres, encuentra un carácter de fin de línea, o llega al final del fichero. Si fgets() lee un carácter de fin de línea, este será parte cadena; además, se añade el carácter nulo al final de la cadena (código ASCII 0x0). Si la función ha podido almacenar algo en cadena, devuelve a su salida la dirección de la propia cadena; en caso contrario, tanto si ha habido algún error como si se ha alcanzado el final de fichero, devuelve NULL.

En el siguiente ejemplo se abre un fichero de texto como lectura, llamado “entrada.txt”. Posteriormente se leen del fichero 9 caracteres que son almacenados en un vector de 10 caracteres. Se almacenan los nueve caracteres leídos más el carácter nulo que indica el final de cadena:

FILE *fich; char cadena[10]; fich = fopen( “c:\\entrada.txt”, “rt” ); fgets( entrada, 10, fich );

Función fgetc

Esta función lee un sólo carácter del fichero indicado por el único parámetro que necesita (de tipo FILE*). Devuelve el carácter leído si no ha habido ningún problema; en caso contrario, devuelve el valor EOF.

Por ejemplo, una llamada a la función fgetc() sería:

char c; FILE * puntero_file; ... c = fgetc(puntero_file);

Función fclose

Esta función cierra el fichero indicado y devuelve 0 si no ha habido ningún problema. Si el fichero no ha podido cerrarse correctamente, devuelve EOF. El prototipo de la función es el siguiente:

int fclose( FILE *puntero_file );

Función feof

Esta función devuelve un valor distinto de cero si la última lectura realizada en el fichero ha intentado sobrepasar el final del mismo; en caso contrario, devuelve cero. Su nombre proviene de las siglas de “File, End

Page 88: capitulo4

194 ♦ Capítulo 4: Fundamentos de Programación.

Of File” (fin de fichero), aunque según acabamos de explicar más bien debería llamarse “File, Last Access Beyond End Of File” (último acceso más allá del fin de fichero).

Su prototipo es el siguiente:

int feof( FILE *puntero_file );

4.7.4 Recorrido secuencial

Con estas funciones ya se pueden ver algunos de los algoritmos básicos de tratamiento de ficheros. El más básico de ellos es el recorrido secuencial; que nos permite leer en secuencia todos los registros del fichero, comenzando con el primero de ellos y terminando cuando se alcanza el fin-de-fichero. El algoritmo general de recorrido secuencial es el siguiente:

Abrir el fichero para lectura SI no se ha podido abrir el fichero HACER: Procesar el error de apertura de fichero TERMINAR FIN-SI Leer un registro del fichero MIENTRAS no se alcance el fin-de-fichero HACER: Procesar el registro leído Leer un registro del fichero FIN-MIENTRAS Cerrar el fichero

El algoritmo se divide en tres fases: la apertura del fichero, el recorrido secuencial y el cierre del fichero. En la fase de apertura, inmediatamente después de abrir el fichero se comprueba si este se ha abierto correctamente; si no es así, el algoritmo debe terminar ya que no se podrán leer sus registros. Las fases de apertura y cierre del fichero son comunes a todos los algoritmos de tratamiento de ficheros que estudiaremos.

La fase del recorrido secuencial se compone de la primera operación de lectura y del bucle MIENTRAS. El bucle nos permite recorrer el fichero sin conocer su longitud, por esa razón la condición de salida del bucle es el fin-de-fichero. Podemos observar que después de cada operación de lectura, el algoritmo comprueba (en la condición del bucle MIENTRAS) si hemos llegado al final del fichero. La condición del bucle MIENTRAS será falsa cuando se intente leer un registro después de haber leído y procesado el último registro del fichero. Por otra parte, si el fichero está vacío (no tiene ningún registro), no se ejecuta el bucle MIENTRAS.

El siguiente programa lee un fichero llamado “texto.txt” y muestra su contenido por pantalla. Cada registro del fichero es un carácter (byte) que se lee con la función fgetc(), y que se procesa mostrándolo por pantalla con printf().

Page 89: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 195

#include <stdio.h> void main(void) { FILE *fich; char caracter; fich = fopen( “texto.txt”, ”rt” ); if (fich == NULL) { printf( “No se ha podido abrir el fichero\n” ); return; } caracter = fgetc( fich ); while (!feof(fich)) { printf( “%c”, caracter ); caracter = fgetc( fich ); } fclose( fich ); }

A partir del recorrido secuencial se pueden realizar otras operaciones más complejas sobre ficheros, como la copia de un fichero en otro, o el procesamiento de los datos de un fichero para guardar los resultados en otro fichero. A continuación se muestran dos ejemplos del último tipo de procesamiento mencionado.

En un primer ejemplo (Figura 4.20) vamos a procesar la información de un fichero de entrada, registro a registro; por cada registro de entrada procesado, se escribe el resultado en un registro del fichero de salida. El proceso lee del fichero de entrada una serie de valores expresados en pesetas, los convierte a euros, y escribe el resultado en el fichero de salida. Los registros del fichero de entrada son números enteros (almacenados como una secuencia de caracteres), el fichero debe contener un número por cada línea.

Conversor Ptas -> Euros

Fichero entrada

1000\n 1324\n 232\n

Fichero salida

6.01\n 7.96\n 1.39\n

Figura 4.20

Page 90: capitulo4

196 ♦ Capítulo 4: Fundamentos de Programación.

void main() { FILE *f_entrada,*f_salida; int pesetas; float euros; f_entrada = fopen( "c:\\pesetas.txt", "rt" ); if (f_entrada == NULL) { printf( “No se puede abrir el fichero de entrada\n” ); return; } f_salida = fopen( "c:\\euros.txt", "wt" ); if (f_salida == NULL) { printf( “No se puede abrir el fichero de salida\n” ); return; } fscanf( f_entrada, "%d", &pesetas ); while( !feof(f_entrada) ) { euros = pesetas / 166.386; fprintf( f_salida, "%.2f\n", euros ); fscanf( f_entrada, "%d", &pesetas ); } fclose( f_salida ); fclose( f_entrada ); }

Otro ejemplo puede ser el siguiente. Imaginemos que tenemos una aplicación de contabilidad que genera diariamente un fichero de movimientos. Dicho fichero (llamado “movimientos.txt”) es un fichero de texto que consta de un registro por cada línea. Cada registro (ver Figura 4.21) está compuesto por: una cadena de 10 caracteres que contiene la fecha en formato día/mes/año (por ejemplo, 12/02/2001), una cadena de 15 caracteres que indica el tipo de movimiento, y un número real (en coma flotante) con el importe del movimiento. Tenemos que escribir, en código C, un programa que abra el fichero de movimientos, muestre cada movimiento por la pantalla, y sume todos los importes. Finalmente, se escribirá la fecha del último movimiento y la suma total en un fichero de texto llamado “totales.txt”.

Fichero de movimientos

Importe Concepto Fecha

12/03/2003Recibo.........1100.43\n 13/03/2003Telefono.......40.03\n 28/03/2003Ingresos.......1495.00\n

Figura 4.21

Page 91: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 197

#include <stdio.h> #define LONG_CADENA 16 #define LONG_FECHA 11 void main(void) { FILE *f_mov, *f_totales; char concepto[LONG_CADENA], fecha[LONG_FECHA]; float importe, total = 0; char salto_linea; f_mov = fopen( "movimientos.txt", "rt" ); if (f_mov == NULL) { printf( “Error al abrir: ‘movimientos.txt’\n” ); return; } fgets( fecha, LONG_FECHA, f_mov ); fgets( concepto, LONG_CADENA, f_mov ); fscanf( f_mov, "%f", &importe ); fscanf( f_mov, "%c", &salto_linea ); while ( !feof(f_mov) ) { total += importe; printf( "%s %s %f\n", fecha, concepto, importe ); fgets( fecha, LONG_FECHA, f_mov ); fgets( concepto, LONG_CADENA, f_mov ); fscanf( f_mov, "%f", &importe ); fscanf( f_mov, "%c", &salto_linea ); } fclose( f_mov ); printf( "Total del dia: %f \n", total ); f_totales = fopen( "totales.txt", "at" ); if (f_totales != NULL) { fputs( fecha, f_totales ); fprintf( f_totales, “ %f\n", total ); fclose( f_totales ); } else printf( “Error al abrir: ‘totales.txt’\n” ); }

Es importante mencionar como se realiza en el código anterior la lectura de cada registro del fichero de movimientos. Cada campo del registro es leído con una sentencia diferente. Los 10 caracteres que forman la fecha se leen con la sentencia:

fgets( fecha, LONG_FECHA, f_mov );

Después se leen los 15 caracteres que forman del concepto con la siguiente sentencia:

fgets( concepto, LONG_CADENA, f_mov );

El importe se lee con la función fscanf(), usando la cadena de formato “%f”:

fscanf( f_mov, "%f", &importe );

Finalmente, leemos con la función fscanf() el carácter de fin-de-línea que señala el final de cada registro, usando la cadena de formato “%c”:

Page 92: capitulo4

198 ♦ Capítulo 4: Fundamentos de Programación.

fscanf( f_mov, "%c", &salto_linea );

Es necesario leer el carácter “fin-de-línea” debido a que, cuando se lee el importe con la función fscanf(), esta lee todos los dígitos que forman el número hasta que se encuentra con el “fin-de-línea”, pero sin leer dicho carácter.

4.7.5 Fusión de ficheros.

El algoritmo de fusión de ficheros se utiliza para crear un fichero ordenado a partir de dos ficheros ordenados. El algoritmo funciona de la misma manera que lo haría una persona cuando mezcla dos pilas de fichas ordenadas para obtener una única pila, también ordenada: se comparan los primeros elementos de cada una de las pilas, y se lleva a la nueva pila (la pila resultado) la ficha que según el criterio de ordenación elegido va la primera. En ese momento una de las pilas de origen tendrá una ficha menos, y se tendrá que volver a comparar la primera ficha de cada pila para elegir la próxima ficha que hay que llevar a la pila resultado. Llegará un momento en el que una de las pilas origen se quede vacía y queden todavía fichas en la otra. Todas esas fichas irán directamente a la pila de resultados.

A continuación se muestra la estructura general del algoritmo de fusión de dos ficheros, cuyos registros están ordenados por algún criterio. Se genera un fichero de resultados que contiene los registros de los dos ficheros de entrada, ordenados por el mismo criterio. Para simplificar el pseudocódigo, vamos a suponer que la apertura de los ficheros se realiza sin problemas.

Abrir el fichero entrada1 para lectura Abrir el fichero entrada2 para lectura Abrir el fichero salida para escritura Leer un registro del fichero entrada1 Leer un registro del fichero entrada2 MIENTRAS no sea fin de fichero de entrada1 ni de entrada2 HACER: SI registro de entrada1 va primero que registro de entrada2 HACER: Escribir el registro de entrada1 en fichero salida Leer un nuevo registro del fichero entrada1 SINO Escribir el registro de entrada2 en fichero salida Leer un nuevo registro entrada2 del fichero entrada2 FIN-SINO FIN-MIENTRAS MIENTRAS no sea fin de fichero de entrada1 HACER: Escribir el registro de entrada1 en fichero salida Leer un nuevo registro del fichero entrada1 FIN-MIENTRAS MIENTRAS no sea fin de fichero de entrada2 HACER: Escribir el registro de entrada2 en fichero salida Leer un nuevo registro del fichero entrada2 FIN-MIENTRAS Cerrar todos los ficheros

A modo de ejemplo veremos un programa que realiza la fusión de dos ficheros de texto que contienen números ordenados de menor a mayor. Se genera un fichero de salida que contiene todos los números de los ficheros de entrada ordenados también de menor a mayor. El proceso se ilustra en un diagrama que aparece tras el código. El código C correspondiente al ejemplo es el siguiente (suponiendo que Entrada1 y Entrada2 son los dos ficheros de entrada, salida es el fichero de salida):

Page 93: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 199

#include <stdio.h> int main (void) { FILE *entrada1, *entrada2, *salida; int entero1, entero2; entrada1 = fopen( "entrada1.txt", "rt"); if (entrada1 == NULL) return -1; entrada2 = fopen( "entrada2.txt", "rt"); if (entrada2 == NULL) return -1; salida = fopen( "salida.txt", "wt"); if (salida == NULL) return -1; fscanf( entrada1, "%d", &entero1 ); fscanf( entrada2, "%d", &entero2 ); while (!feof(entrada1) && !feof(entrada2)) { if (entero1 <= entero2) { fprintf( salida, "%d\n", entero1 ); fscanf( entrada1, "%d", &entero1 ); } else { fprintf( salida, "%d\n", entero2 ); fscanf( entrada2, "%d", &entero2 ); } } while (!feof(entrada1)) { fprintf( salida, "%d\n", entero1 ); fscanf( entrada1, "%d", &entero1); } while (!feof(entrada2)) { fprintf( salida, "%d\n", entero2 ); fscanf( entrada2, "%d", &entero2 ); } printf("\nMezcla realizada\n"); fclose( entrada1 ); fclose( entrada2 ); fclose( salida ); return 0; }

A continuación en la Figura 4.22, se ilustra con un gráfico el proceso de la fusión, donde las celdas sombreadas representan los números leídos en cada iteración.

Page 94: capitulo4

200 ♦ Capítulo 4: Fundamentos de Programación.

01 40 71 77 91Entrada1:

34 39 71Entrada2: 01Salida:

01 40 74 77 91Entrada1:

34 39 71Entrada2: 01 34Salida:

01 40 74 77 91Entrada1:

Entrada2: 01 34 39Salida:

01 40 74 77 91Entrada1:

34 39 71Entrada2: 01 34 39 40 Salida:

01 40 74 77 91Entrada1:

Entrada2: 01 34 39 40 71 Salida:

01 40 74 77 91Entrada1:

34 39 71Entrada2: 01 34 39 40 71 74 77 91 Salida:

34 39 71

Iteración 1

34 39 71

Iteración 2

Iteración 3

Iteración 4

Iteración 5

Iteración 6 y sucesivas

Figura 4.22

4.8 TIPOS DEFINIDOS POR EL USUARIO

En C se permite crear tipos de datos heterogéneos de dos formas: agrupando varias variables en una sola llamada estructura o bien utilizando una variable llamada unión que permite a varias variables compartir una misma zona de memoria. También se pueden crear nombres nuevos para tipos de variable utilizando typedef. Otra extensión del estándar ANSI C lo constituyen las enumeraciones. Estas pueden verse como una colección de constantes enteras, que van a tener valores sucesivos.

4.8.1 Estructuras

Una estructura es un conjunto de campos que se designan con un único nombre. Cada campo juega el papel de una variable de otro tipo (int, char, double, etc., o incluso otro tipo más complejo), aunque en realidad no lo es, ya que la verdadera variable será el conjunto de todos los campos. Esto es muy útil cuando queramos mantener junta información relacionada. Para definir una estructura, utilizamos la palabra clave struct. Esta palabra le indica al compilador que lo que viene a continuación es una estructura. La forma general para definir una estructura es la siguiente:

Page 95: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 201

struct nombre_estructura { tipo campo1; tipo campo2; tipo campo3; . . tipo campo_n; } lista_variables_estructura;

De esa forma, cada una de las variables definidas en lista_variables_estructura tendrá todos los campos de la estructura, es decir, la forma de la Figura 4.23.

variable tipo estructura

campo1

campo2

campo3

...

campo_n

Figura 4.23

Con esta definición realmente hacemos dos cosas: por una parte, estamos definiendo una plantilla de estructura, la cual tomará el nombre que indique nombre_estructura, y por otra parte, estamos definiendo una serie de variables con la forma de la plantilla definida. Estas variables vendrán dadas por lista_variables_estructura. Un ejemplo concreto podría ser la siguiente estructura:

struct Datos_Persona { char nombre[20]; char apellido1[20]; char apellido2[20]; unsigned long DNI; char calle[56]; char ciudad[20]; } pers1, pers2, pers3, pers4;

Cada una de las variables declaradas (pers1, pers2, pers3, pers4) tendrá la forma de la estructura Datos_Persona, y ocupará en memoria los bytes que sumen todos los campos. Según se ve en la Figura 4.24, cada variable ocupará 140 bytes :

Page 96: capitulo4

202 ♦ Capítulo 4: Fundamentos de Programación.

variables pers1, pers2, pers3, pers4, tipo estructura Datos_Persona

calle

nombre apellido1

apellido2 DNI

ciudad

40 bytes

Figura 4.24

Si posteriormente deseamos crear más variables del tipo estructura Datos_Persona, bastaría con declararla de la siguiente forma:

struct Datos_Persona nueva_persona;

También se pueden inicializar los campos de una variable tipo estructura usando la notación con llaves {}, de forma similar a los vectores:

struct Datos_Persona pers_sevillana = {"David","Diaz","Dominguez","12345678","Percebe,13",“Sevilla”};

Hay que tener cuidado al elegir el lugar donde definimos la plantilla de estructura. Lo más normal es declararla antes y fuera del main() para que pueda ser utilizada en cualquier lugar del programa (sería una plantilla de ámbito global). Si se declarara dentro de una función concreta (incluyendo el caso de dentro del main()), la plantilla sería sólo local a tal función, no pudiendo usarse en ninguna otra parte.

A la hora de definir la plantilla de estructura podemos omitir, o bien, el nombre_estructura, o bien, la lista_variables_estructura. En el caso de omitir nombre_estructura, sólo podremos usar la plantilla una vez (para definir las variables que aparezcan en la lista_variables_estructura); por eso omitir nombre_estructura no es habitual.

Si es común omitir la lista_variables_estructura. Entonces, habremos creado una plantilla de estructura sin variables asociadas, sin embargo podremos declarar variables en cualquier momento, usando el modo de declaración visto anteriormente, es decir:

struct nombre_estructura nombre_variable_nueva;

En ningún caso podremos omitir, a la vez, el nombre_estructura y la lista_variables_estructura.

Para poder acceder a los elementos individuales que forman las estructuras, se utiliza el operador ‘.’. Veamos un ejemplo de acceso, suponiendo que tenemos la siguiente estructura:

struct punto { double x; double y; double z; } a, b, c;

Para asignar valores a la variable tipo estructura a (que representa un punto en el espacio R3) bastaría con hacer:

Page 97: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 203

a.x = 5; a.y = 23; a.z = 10.5;

Otra operación que podremos realizar son asignaciones entre variables estructuras con la misma plantilla:

c = a; b = c;

Lo anterior equivale a las asignaciones de todos los campos de la estructura (es decir, equivale a c.x=a.x; c.y= a.y; c.z= a.z, etc.)

Note que cada campo es como una variable tipo double, y a partir de aquí se puede operar con él como si fuera otro double más. Por ejemplo:

double prod_escalar; prod_escalar = a.x * b.x + a.y *b.y + a.z * b.z ;

sería el producto escalar de los vectores a, b.

Una función también puede devolver una estructura completa y puede tener un parámetro del tipo estructura. Por ejemplo, la siguiente función escalado() se ha definido para que devuelva un punto, que sea el escalado del punto dado por el primer parámetro, y según la escala dada por el segundo parámetro:

struct punto escalado (struct punto punt1, double escala) { struct punto punt2; punt2.x = punt1.x * escala; punt2.y = punt1.y * escala; punt2.z = punt1.z * escala; return punt2; }

Los tipos de los campos de una estructura pueden ser simples (int, float, char, …) o complejos (vectores, matrices, otra estructura, …). Veamos un ejemplo de una plantilla de estructura que combina elementos individuales simples y complejos.

struct fecha { int dia; int mes; int anyo; }; struct ficha { char nombre[20]; char apellidos[40]; struct fecha dia_nacimiento; };

Si queremos definir una variable tipo struct ficha llamada persona1, y queremos poner la fecha de nacimiento haremos:

Page 98: capitulo4

204 ♦ Capítulo 4: Fundamentos de Programación.

struct ficha persona1; persona1.dia_nacimiento.dia = 20; persona1.dia_nacimiento.mes = 2; persona1.dia_nacimiento.anyo = 2001;

La variable persona1 tendría la forma de la Figura 4.25:

campo dia_nacimiento, tipo estructura fecha

variable persona1, tipo estructura ficha

apellidos

nombre

dia

20 bytes

anyomes

Figura 4.25

Otra posibilidad frecuentemente utilizada es la definición de vectores (arrays o tablas) o matrices de estructuras. De esta manera se permite gestionar una gran cantidad de información fácilmente. Un ejemplo sencillo de esto sería por ejemplo una matriz de puntos. Teniendo en cuenta la estructura punto definida más arriba podríamos definir la siguiente matriz:

struct punto matriz1[10][10];

donde cada componente de tal matriz es un punto en R3 del tipo struct punto.

Una posible inicialización, de la anterior matriz, en la que todos sus componentes toman como valor el origen de coordenadas, sería la siguiente:

int cont_filas, cont_columnas; for (cont_filas=0; cont<10; cont_filas++) { for (cont_columnas=0; cont_columnas<10; cont_columnas++) { matriz[cont_filas][cont_columnas].x = 0.0; matriz[cont_filas][cont_columnas].y = 0.0; matriz[cont_filas][cont_columnas].z = 0.0; } }

De forma análoga, se pueden crear variables más complicadas a partir de la estructura ficha. Por ejemplo, se podrían gestionar los datos de toda una población de individuos, declarando una variable que fuera un vector de estructuras ficha:

struct ficha poblacion[100]; // Vector que sirve para // gestionar los datos personales // de 100 individuos.

Con lo anterior, para asignar una ‘F’ a la primera letra del nombre de la persona número 41 almacenada en el vector anterior deberíamos hacer:

Page 99: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 205

poblacion[40].nombre[0] = ‘F’; //recuerde que en C se enumera //desde el 0

4.8.2 Punteros a estructuras

Al igual que con el resto de tipos de variables, C permite crear variables punteros a estructuras. La forma de declarar estas variables es análoga a la de cualquier puntero:

struct nombre_estructura *nombre_variable_estructura;

Para poder acceder a los elementos a los que apunta un puntero a estructura18, se utiliza por supuesto el operador indirección ‘*’, pero hay que poner un paréntesis alrededor de tal operador y la variable puntero, ya que la prioridad del operador ‘.’ es superior a la del ‘*’. Por tanto, un ejemplo de punteros a estructuras, y del operador indirección sería:

struct punto a, b, *p, *q; a.x = 5; a.y = 23; a.z = 10; p = &b; /* &b nos devuelve la direccion de la estructura b */ (*p).x = 2; (*p).y = a.y; (*p).z = a.z; /* con las tres asignaciones anteriores, estamos inicializando la estructura b, pero a traves del puntero p */ q = p; /* p y q apuntan a la misma estructura b */

Al igual que ocurría con los punteros a tipos básicos de datos, también se pueden pasar punteros a estructuras en las llamadas a funciones. Así por ejemplo tendríamos:

18 También se puede usar el operador flecha ‘->’ (formado por un signo menos seguido de un signo de mayor, sin ningún espacio en blanco entre ellos). La inicialización de la estructura b se podría también hacer así:

p->x = 2; //equivale a (*p).x = 2 p->y = a.y; p->z = a.z;

Page 100: capitulo4

206 ♦ Capítulo 4: Fundamentos de Programación.

int main(void) {

struct punto coordenada1, coordenada2, *p_coordenada1; void cambiar_a_punto_simetrico_en_planoXY (struct punto *); coordenada1.x = 5.1; coordenada1.y = -11.2; coordenada1.z = 0; p_coordenada1 = &coordenada1; coordenada2 = coordenada1; cambiar_a_punto_simetrico_en_planoXY (p_coordenada1); //otra llamada usando el operador direccion & cambiar_a_punto_simetrico_en_planoXY (&coordenada2); return 0; } /* La siguiente funcion intercambia la coordenada x por la y */ void cambiar_a_punto_simetrico_en_planoXY (struct punto *c) { (*c).x = -(*c).x; (*c).y = -(*c).y; }

4.8.3 Uniones

En el lenguaje C, cuando hablamos de unión nos estamos refiriendo a una porción de memoria compartida por varias variables, las cuales pueden ser de distintos tipos. Para declarar una unión usamos la palabra clave union de la siguiente forma:

union nombre_union { tipo campo1; tipo campo2; … tipo campoN; }lista_variables_union;

Como se ve, la forma de declaración es análoga a la de una estructura, de forma que podremos declarar una variable, o bien colocando su nombre al final de la declaración inicial, o posteriormente, utilizando una declaración aparte como muestra el siguiente código:

union nombre_union nombre_variable_nueva;

Cuando definimos una unión y declaramos variables asociadas, el compilador reservará para cada variable, un espacio de memoria cuyo tamaño coincidirá con el tamaño del elemento más grande que se defina dentro de la unión. Supongamos que tenemos la siguiente declaración:

union prueba_union { char letra; char cadena[8]; short int numero; } u1;

El compilador reservará, para la variable u1, 8 bytes de memoria (que corresponde con el tamaño del elemento cadena). Suponiendo que la variable numero ocupa 2 bytes, la organización en memoria de la unión sería la que muestra la Figura 4.26:

Page 101: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 207

0 1 2 3 4 5 6 7

bytes

cadena

letra

numero

Figura 4.26

De forma que en caso de acceder al elemento letra, también estaremos accediendo al primer byte del campo cadena, mientras que si accedemos a numero, estaremos accediendo a los dos primeros bytes de cadena. Por último, si accedemos a cadena estaremos accediendo a todos los bytes de la variable u1.

4.8.4 Enumeraciones

Una enumeración es un conjunto de constantes enteras con nombre, que especifica todos los valores válidos que una variable de este tipo puede tener. La forma de definir una enumeración es:

enum nombre_enum {lista_de_enum} variables_de_enum;

Veamos un ejemplo:

enum color {blanco, amarillo, rojo, verde, azul, marron} color_mesa;

Como con las estructuras, C permite definir posteriormente más variables enumeradas del tipo color, la forma de hacerlo sería la siguiente:

enum color color_coche;

Una vez definidas las variables enumeradas, las siguientes operaciones serían válidas:

color_mesa = verde; color_coche = color_mesa; if(color_mesa == azul) printf(“La mesa es de color azul\n”); switch(color_coche) { case blanco : printf(“El coche es de color blanco\n”); case amarillo: printf(“El coche es de color amarillo\n”); case rojo : printf(“El coche es de color rojo\n”); case verde : printf(“El coche es de color verde\n”); case azul : printf(“El coche es de color azul\n”); case marron : printf(“El coche es de color marron\n”); }

4.8.5 Definición de nuevos nombres de tipos (typedef)

Para poder definir nuevos nombres de tipos de datos, usamos la palabra clave typedef. Con esto, realmente no estamos creando un nuevo tipo de datos, sino que se define un nuevo nombre a un tipo ya existente. También podemos usar typedef para asignar un nombre de tipo a una estructura o a una enumeración, de forma que para declarar las variables de estos tipos no necesitaremos precederlos de las palabras clave struct o enum, respectivamente. La forma general de la sentencia typedef es:

Page 102: capitulo4

208 ♦ Capítulo 4: Fundamentos de Programación.

typedef tipo nombre_nuevo_tipo;

Algunos ejemplos podrían ser:

typedef int ENTERO; typedef unsigned char BYTE; typedef struct fecha FECHA;

De forma que podremos definir variables como:

ENTERO *e1, e2; //e1 es un puntero a int, e2 es un int BYTE *b1, b2; FECHA *f1, f2, lista_fechas[20];

Un ejemplo ya conocido de uso de typedef son las variables de manejo de ficheros FILE *. En realidad, el tipo FILE está definido usando typedef en el fichero stdio.h, como una estructura con diversos campos para poder trabajar con un fichero.

4.9 INTRODUCCIÓN AL DISEÑO MODULAR

Hasta ahora hemos visto y usado programas lo suficientemente sencillos como para escribirlos en un único fichero de texto fuente, que editamos nosotros mismos con el editor integrado en el entorno de desarrollo o con un editor externo. Sin embargo, cuando el tamaño del proyecto software lo requiere, o bien cuando las necesidades del programa lo exigen, tenemos que recurrir a trabajar con trozos o partes. Como cada una de estas partes suele tener su propia entidad, diferente de las demás, se suele usar el término módulo. La metodología para construir una aplicación a partir de módulos se llama diseño modular. En cualquier caso, sea cual sea la metodología de programación que usemos, no hay que olvidar que un programa en C debe tener una, y sólo una, función main().

Son varias las causas que lo hacen necesario:

El proyecto es grande y debe ser acometido por un grupo de programadores que trabajarán en paralelo.

La aplicación contiene partes no escritas por nosotros, que hemos comprado o cuya licencia hemos solicitado a terceros.

La aplicación debe ser fácilmente portable (ver capítulo 1) a otras plataformas informáticas, y nos interesa separar la parte de aplicación que es independiente de la plataforma de aquélla que es dependiente de plataforma, para sólo tener que rescribir esta última al realizar la traslación, transferencia o porte.

Queremos que nuestra aplicación o parte de ella pueda ser usada en otros programas desarrollados por terceros.

Veremos cómo debe estructurarse nuestro código en más de un módulo o fichero de texto fuente de lenguaje C, y cómo se pueden combinar todos esos archivos para formar la aplicación o ejecutable final.

4.9.1 El proyecto C

Un proyecto C es una colección de ficheros necesarios para poder crear un fichero ejecutable o aplicación. Hasta ahora, casi sin notarlo, hemos usado muchos archivos en la generación de una simple aplicación como las mostradas en los apartados precedentes: sólo el hecho de usar la directiva #include implica añadir al menos un fichero más al nuestro. Estos archivos pueden ser: archivos de código fuente en C, archivos de código fuente en otros lenguajes (incluyendo el ensamblador), archivos de biblioteca y archivos tipo OBJ precompilados (objetos).

Dependiendo de la plataforma de desarrollo utilizada, será más o menos sencilla la tarea de añadir archivos a un proyecto. La gran mayoría de entornos modernos de desarrollo disponen de soporte de gestión de proyectos, permitiendo así fácilmente añadir o quitar ficheros a un determinado proyecto. A efectos prácticos, en

Page 103: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 209

esta sección se obviarán los detalles de la gestión de proyecto, ya que ésta es diferente para cada entorno de desarrollo. Para ilustrar los ejemplos en los que se necesite saber que ficheros forman parte de un proyecto, se mostrará únicamente un listado de éstos.

Según se muestra en la Figura 4.27, un ejecutable es el resultado de enlazar uno o más módulos de tipo objeto junto con ficheros de biblioteca del sistema. Esos módulos objeto pueden ser producto de la compilación de módulos fuente, o bien pueden existir desde antes. Esto implica que existen una serie de relaciones entre los distintos archivos que toman parte en el proceso de compilación. Esas relaciones se suelen denominar dependencias y se muestran en la Figura 4.27 como flechas.

Aunque en un entorno de desarrollo la gestión de proyectos se realice de una manera visual, lo que realmente se está haciendo es definir un fichero llamado Makefile (del inglés “generar fichero”, se entiende que es el ejecutable) en el que se listan los archivos de que consta nuestro proyecto y las dependencias entre ellos. Un archivo Makefile simple podría ser el siguiente:

programa1: fuente1.c cc –o programa1 fuente1.c

Un fichero Makefile define una serie de objetivos. Un objetivo es un fichero de destino que se debe generar a partir de unos ficheros origen, realizando una serie de traducciones y operaciones. En el ejemplo tenemos un Makefile con un fichero objetivo: programa1. Este fichero objetivo depende de que exista un fichero llamado fuente1.c. Es decir, la clave de la gestión de proyectos reside básicamente en la elaboración del fichero Makefile. Una vez creado éste, y si los todos ficheros origen existen, se ejecuta el comando u orden make (“crear”) del compilador para generar todos los objetivos. Si alguna de las compilaciones fallan, no se generará ningún fichero ejecutable y la orden make terminará con un error.

Fichero ejecutable

Ficheros fuente C

Fichero fuente ASM

Compilador lenguaje C

Compilador o ensamblador lenguaje ASM

Fichero OBJ

Fichero OBJ (del ASM)

Ficheros OBJ (de C y H)

Ficheros OBJ de librería del sistema

Enlazador

Archivos incluidos en la definición del proyecto

Figura 4.27

Como se puede ver en la Figura 4.27 el fichero ejecutable definitivo se generaría a partir de los siguientes ficheros (sombreados en la figura): varios ficheros fuente de código C, un fichero fuente ASM (que habría que compilar), y un fichero objeto previamente existente. Por lo tanto, en la definición del fichero Makefile estarían los siguientes ficheros:

Page 104: capitulo4

210 ♦ Capítulo 4: Fundamentos de Programación.

Definición del proyecto: - Ficheros C - Fichero fuente ASM - Fichero OBJ (previamente existente e incluido por el usuario).

En la definición anterior, suponemos que al ejecutar la orden make, se puede lanzar también la compilación o ensamblado de los ficheros ASM (la mayoría de plataformas de desarrollo lo permiten). Si no fuera así, primero debería compilarse el ASM, para incluir directamente en el Makefile el archivo OBJ (proveniente del ASM).

Como se puede ver, los ficheros origen que dan lugar a un fichero ejecutable, pueden ser de una naturaleza bastante variada, permitiendo así que el programador pueda utilizar varios lenguajes (si le es más conveniente), e incluso reutilizar antiguos archivos tipo OBJ, para generar su aplicación.

4.9.2 Creación de módulos de código.

Es inevitable que cuando un código fuente es tan grande que ha de ser dividido en varias partes (módulos o archivos fuente en definitiva), unas partes de código necesiten funciones que estén en otras. De esta forma, unos ficheros fuente dependen del código existente en otros ficheros fuente. Como ya se vio en el apartado anterior, la definición de proyecto permite al programador crear un fichero ejecutable a partir de varios ficheros objeto. Sin embargo, la definición del proyecto especifica únicamente qué ficheros objeto deben de existir o ser generados por el compilador. La definición del proyecto no dice dónde encontrar la función que se necesita para compilar un determinado fichero fuente. Ilustremos lo anterior con un ejemplo sencillo.

Imagine que tiene un proyecto con dos ficheros fuente, principal.c y dividir.c, cuyo contenido se presenta a continuación, y con el propósito de dividir varios números, pero utilizando cierta función definida para ello.

Fichero dividir.c:

int error; float dividir (float dividendo, float divisor) { float cociente; if (divisor==0) error = 1; else { error = 0; cociente = dividendo/divisor; } return cociente; }

Fichero principal.c:

Page 105: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 211

#include <stdio.h> void main (void) { float a=1.45, b=3.2, c; c = dividir(a, b); if (error) printf (“Error de division por 0\n”); else printf (“El cociente de la division es %f\n”, c); }

La definición del proyecto estaría formada por los siguientes ficheros:

principal.c dividir.c

En un grafo de dependencias donde la flecha gruesa y rellena indica “depende de”, las dependencias entre ficheros fuentes serían las de la Figura 4.28.

principal.c

dividir.c

Figura 4.28

Como se puede ver en los listados, principal.c necesita la función dividir() y la variable error del fichero dividir.c. Así pues, la compilación de dividir.c no tendría problemas. Sin embargo, la de principal.c no podría realizarse ya que le faltan las definiciones de dos entidades: la función dividir() y la variable error. En general, cuando se trabaja con diseño modular, las entidades que conducen a las dependencias se denominan símbolos públicos (recuerde que el compilador llama símbolo a cualquier cosa declarada por el programador, es decir, a los nombres de las variables y de las funciones). Por tanto, en nuestro caso el módulo principal.c necesita los símbolos dividir y error.

¿Cómo resolver este problema? Cuando un fichero necesita símbolos de otro se debe crear un fichero fuente en lenguaje C, con los prototipos de las funciones y las declaraciones de las variables que se necesitan. Este tipo de ficheros se denominan “ficheros de cabecera”, puesto que suelen ponerse al principio de otros ficheros fuente (en la cabecera), y por convenio, llevan la extensión .h.

De esta manera, incluyendo el fichero de cabecera que hemos llamando dividir.h, en el fichero que necesita los símbolos (en este caso principal.c), se logra solucionar el problema. Para incluir un fichero dentro de otro, ya se imaginará que debe usar la conocida una directiva #include. Tal vez ya se haya dado cuenta que la famosa línea de código con que hemos empezado casi todos los programas de este libro (#include <stdio.h>), estaba incluyendo un fichero de cabecera con los símbolos necesarios para utilizar la entrada/salida estándar (en inglés “standar input/output”).

Por lo tanto, el código de nuestro ejemplo quedaría como:

Fichero dividir.c:

Page 106: capitulo4

212 ♦ Capítulo 4: Fundamentos de Programación.

#include “dividir.h” float dividir (float dividendo, float divisor) { float cociente; if (divisor==0) error = 1; else { error = 0; cociente = dividendo/divisor; } return cociente; }

Fichero dividir.h:

int error; float dividir(float dividendo, float divisor);

Fichero principal.c:

#include <stdio.h> #include “dividir.h” void main (void) { float a=1.45, b=3.2, c; c = dividir(a, b); if (error) printf (“Error por dividir por 0\n”); else printf (“El cociente de la division es %f\n”, c); }

La Figura 4.29 muestra gráficamente la inclusión de ficheros de este ejemplo.

dividir.c #include “dividir.h” float dividir(float dividen { float cociente;

principal.c #include <stdio.h> #include “dividir.h” void main (void) { float a,b,c;

dividir.h int error; float dividir(float dividen

Figura 4.29

De esta forma, principal.c tiene la declaración de la variable error y el prototipo de la función dividir(), ya que mediante la directiva #include ambos han sido insertados en el contenido del propio

Page 107: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 213

código fuente. Ahora bien, según este último ejemplo, error es una variable que está declarada dentro de los dos ficheros fuente (dos veces), ya que la directiva #include se ha puesto en principal.c y en dividir.c. Según la especificación del estándar ANSI-C, una variable sólo puede estar declarada una vez (en un único módulo), por lo cual, de momento obtendríamos un error al enlazar los dos ficheros fuentes del proyecto. El error sería del tipo “símbolo duplicado” o similar. En algunos compiladores como gcc, que no siguen el estándar ANSI-C, este ejemplo funcionaría, ya que ellos cuando un símbolo está duplicado, lo definen (se reserva memoria para tal variable) exclusivamente en uno de los módulos. El resto de módulos lo pueden usar, pero no lo definen (no existe en la realidad tal variable; no se reserva memoria para ella). Pero no así en el estándar ANSI-C y en la mayoría de compiladores, para los que tendremos que proceder como se explica en el siguiente apartado.

Con la función dividir(), la cosa es diferente, puesto que ésta se ha definido sólo una vez (el cuerpo de la función está en dividir.c). Sin embargo, el hecho de que su prototipo esté en los dos ficheros fuente, no genera ningún error. Todo esto se verá con más detalle en el siguiente apartado.

4.9.3 Exportación de símbolos.

Al estudiar las reglas de ámbito de variables, vimos que éstas podían ser globales o locales. Las globales lo son para todo el fichero (o según nuestra nueva terminología, módulo) y las locales sólo son visibles dentro del cuerpo entre las llaves {} donde se declaran (típicamente dentro de una función). Ahora nos planteamos el que una variable o una función pueda ser utilizada o accedida por otra función que esté en otro módulo. Para ello, esa variable o función debe estar declarada como pública. Como el propio adjetivo indica, cuando un símbolo es público, éste será conocido por todos los ficheros o módulos del proyecto. En lenguaje C automáticamente son símbolos públicos todas las funciones y todas las variables globales. Por el contrario, las variables locales no serán nunca públicas, sino privadas, y nunca una variable local de un módulo se podrá utilizar en otro.

En compiladores que siguen el estándar ANSI-C, para evitar el error comentado anteriormente de “símbolo duplicado”, se debe usar la palabra clave extern. Esta palabra clave puesta al principio de la declaración de una variable, indica al compilador que la misma está definida en otro módulo. Se dice entonces que se está importando el símbolo desde otro módulo (que sería el exportador).

Sin embargo, para escribir una declaración externa de una función basta con su prototipo; no hay que usar la palabra extern delante de la función. De esta forma, el fichero dividir.h quedaría como:

extern int error; float dividir(float dividendo, float divisor);

El uso de una declaración extern implica que debe de existir otra declaración de esa misma variable sin dicha palabra clave. Es decir, se pueden hacer varias declaraciones en módulos diferentes de la misma variable poniendo extern, pero en uno de ellos (el auténtico “propietario” de la variable) esta palabra no puede aparecer. En la Figura 4.30 se puede ver gráficamente esta metodología de diseño.

Page 108: capitulo4

214 ♦ Capítulo 4: Fundamentos de Programación.

Módulo 1 extern int variable; ... ...

Módulo 3 int variable; ... ...

Con esta declaración se le dice al compilador que la variable existe, pero que su declaración está en otro módulo.

Módulo 2 extern int variable; ... ...

Puede haber tantas declaraciones extern de la misma variable como se quiera, siempre que cada una esté en un módulo diferente.

Sin embargo una de ellas no tiene que ser extern. Este es el módulo que REALMENTE declara la variable y la crea en memoria.

Figura 4.30

Por lo tanto, para que nuestro ejemplo sea correcto, no se puede poner extern en la declaración existente en dividir.h, puesto que todas las declaraciones de la variable error serían extern, y no habría ningún módulo propietario de la misma. Una primera solución a este problema podría ser la siguiente:

Fichero dividir.c:

#include “dividir.h” int error; float dividir (float dividendo, float divisor) { float cociente; if (divisor==0) error = 1; else { error = 0; cociente = dividendo/divisor; } return cociente; }

Fichero dividir.h:

float dividir(float dividendo, float divisor);

Fichero principal.c:

Page 109: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 215

#include <stdio.h> #include “dividir.h” extern int error; void main (void) { float a=1.45, b=3.2, c; c = dividir(a, b); if (error) printf (“Error por dividir por 0\n”); else printf (“El cociente de la division es %f\n”, c); }

Como se puede ver, no se puede incluir la declaración del símbolo error en el fichero de cabecera pues la variable sería extern para los dos módulos. Esta no es la única solución al problema. Otra solución sería tener dos ficheros de cabecera, cada uno con una declaración diferente. Las dificultades y engorros vienen cuando un proyecto tiene un tamaño considerable (por poseer un gran número de ficheros y de símbolos globales). Seguramente un proyecto grande contiene muchos símbolos que deben ser exportados, y cualquier cambio en la lista de símbolos, obligaría a cambiar al menos dos ficheros. La solución más avanzada y cómoda es mantener la declaración en el fichero de cabecera, y que ésta cambie dependiendo de si se incluye en principal.c o en dividir.c. Así se puede conseguir el mismo efecto con un único fichero, si su usa la compilación condicional.

4.9.4 Compilación condicional.

La compilación condicional permite que en un mismo fichero fuente se puedan compilar conjuntos diferentes de líneas de código dependiendo de una determinada condición.

Para la compilación condicional, el lenguaje C cuenta con varias directivas. Ya conocemos un par de directivas: #include y #define. Se les da el nombre de directivas porque realmente no son código, si no cambios que han de hacerse antes de compilar. Por eso quien “lee”, procesa y actúa en consecuencia de tales directivas no es el compilador, sino otro programa (que todos los entornos de desarrollo contienen) llamado preprocesador.

Para la compilación condicional veamos primero la directiva #ifdef y la directiva #ifndef, cuya sintaxis básica es la siguiente (véase el apartado 4.10 de este capítulo):

#ifdef NOMBRE_DE_CONSTANTE lineas_de_codigo_a_compilar_si_la_constante_esta_definida #endif

El nombre de la directiva #ifdef proviene del inglés “si está definido”. Como indica su nombre, si antes de un #ifdef hay una línea de código que haya definido NOMBRE_DE_CONSTANTE (usando la directiva #define), se compilarán las líneas de código que están entre #ifdef NOMBRE_DE_CONSTANTE y #endif. De lo contrario, dichas líneas no se compilarán (será como si el compilador no las leyese).

Una sintaxis más completa de esta directiva es la siguiente:

#ifdef NOMBRE_DE_CONSTANTE lineas_de_codigo_a_compilar_si_la_constante_esta_definida #else lineas_de_codigo_a_compilar_si_la_constante_NO_esta_definida #endif

Como se habrá imaginado, el funcionamiento del preprocesador en este último caso es análogo al anterior, pero esta vez se compilará el primer grupo de líneas de código cuando la constante NOMBRE_DE_CONSTANTE esté definida, y el segundo cuando no lo esté.

Page 110: capitulo4

216 ♦ Capítulo 4: Fundamentos de Programación.

A continuación se presenta la sintaxis de #ifndef (del inglés “si no está definido”), que es análoga a la de #ifdef, pero que funciona a la inversa. Es decir, con #ifndef las líneas de código se compilan cuando la constante no está definida (véase el apartado 4.10 de este capítulo).

#ifndef NOMBRE_DE_CONSTANTE lineas_de_codigo_a_compilar_si_la_constante_NO_esta_definida #endif

Análogamente, también tiene una sintaxis extendida:

#ifndef NOMBRE_DE_CONSTANTE lineas_de_codigo_a_compilar_si_la_constante_NO_esta_definida #else lineas_de_codigo_a_compilar_si_la_constante_esta_definida #endif

Con estas directivas ya se puede implementar la solución más avanzada que se proponía más arriba. Podría ser, por ejemplo, algo así:

Fichero dividir.c:

#define MODULO_PROPIETARIO #include “dividir.h” float dividir (float dividendo, float divisor) { float cociente; if (divisor==0) error = 1; else { error = 0; cociente = dividendo/divisor; } return cociente; }

Fichero dividir.h:

float dividir(float dividendo, float divisor); #ifdef MODULO_PROPIETARIO int error; #else extern int error; #endif

Fichero principal.c:

Page 111: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 217

#include <stdio.h> #include “dividir.h” void main (void) { float a=1.45, b=3.2, c; c = dividir(a, b); if (error) printf (“Error por dividir por 0\n”); else printf (“El cociente de la division es %f\n”, c); }

Como se puede intuir de los listados, la constante MODULO_PROPIETARIO se utiliza para distinguir el módulo o fichero que es propietario de la variable error. El resto de módulos, al no incluir tal constante, declararán la variable como externa. Por lo tanto, dicha estrategia no funcionaría si, o bien no se declara en ninguno de los dos ficheros fuente dicha constante, o bien se declara en ambos. Al igual que se advirtió para las variables globales en el apartado 4.5 cuando se estudió el ámbito de una variable dentro de un fichero, tenga cuidado con llamar con el mismo nombre a una variable del propio módulo y a una variable externa. Un buen consejo es declarar con un nombre largo y distintivo las variables que pertenecen a más de un módulo. Por ejemplo, si nuestras funciones pertenecieran a un proyecto grande, podríamos denominar la variable en cuestión algo como: error_proyecto o error_externo.

4.9.5 Consideraciones para proyectos de gran envergadura.

Los conceptos tratados en este apartado permitirán al programador abordar el desarrollo de proyectos de programación relativamente grandes. Como ya se ha visto, el hecho de poder dividir un programa grande en varios módulos de código aporta claridad y lo que es más importante, facilidad de mantenimiento. Por mantenimiento de un proyecto se entiende el realizar cambios en alguna parte del mismo, ya sea, para ampliarlo, mejorarlo, adaptarse a otra máquina, o tras detectar errores. El mantenimiento de un proyecto grande suele ser tan costoso en tiempo como la codificación o escritura de todo el programa (de su primera versión, antes de hacer cambios para mantenerlo).

Además, un correcto y adecuado diseño, refiriéndonos aquí a la división del programa grande en módulos, permite la reutilización de un mismo código de manera que pueda ser referenciado (o llamado) desde más de un módulo. De esa manera se puede ahorrar mucho trabajo, al no tener que escribir varias veces funciones similares.

Imagine que está creando un videojuego. Decide dividir el programa creando un módulo para definir el movimiento de cada tipo de personaje que sale por la pantalla. Todos estos módulos llaman a un conjunto común de funciones que son las que dibujan o “imprimen” el personaje (sea cual sea) en la pantalla. Además, existe un módulo principal que es el que llama a las funciones de movimiento de cada personaje para que se muevan adecuadamente. El grafo de dependencias de código sería el que muestra la Figura 4.31 (recuerde que la flecha quiere decir “depende de”).

Page 112: capitulo4

218 ♦ Capítulo 4: Fundamentos de Programación.

principal.c

Personaje2.c Personaje_n.c Personaje1.c

Pantalla.c

...

Figura 4.31

De esta forma, el módulo pantalla es único y es utilizado por todos los módulos de movimiento de personajes. Esta decisión de diseño ahorra al programador la escritura de una gran cantidad de líneas de código (las correspondientes a escribir las mismas funciones de pantalla para cada uno de los módulos), y facilita el mantenimiento del módulo de pantalla, ya que si hay que corregirlo o modificarlo, sólo habrá que tocar en un único fichero y no en varios.

Otro aspecto que cabe destacar es el siguiente: un módulo puede referenciar a otro, y éste a su vez, a otro o a otros. Hasta ahora, la sencillez de los ejemplos no dejaba ver esto con claridad, pero se hace evidente en este ejemplo. De esta manera, la estructura de dependencias que puede tener un mismo proyecto puede ser tan complicada como el programador quiera. En general, la elección de una determinada estructura de dependencias, se realiza atendiendo a cuál llevará a un código más fácil de mantener para el programador. Habrá ocasiones en las que el programa sea tan simple que no merezca la pena hacer módulos separados. Sin embargo otras, dada la complejidad del código o a que existen funciones comunes que se utilizan en muchos lugares, el programador estará obligado a “modularizar” su programa.

4.10 EL PREPROCESADOR ESTANDAR EN LENGUAJE C

El preprocesador va a analizar el fichero fuente que pretendemos compilar, antes de la compilación real, de forma que realizará las sustituciones de las macros y procesará las directivas que se encuentre en dicho fichero fuente.

La forma que tenemos distinguir una línea que contiene una directiva de preprocesado con el resto de líneas, es que tiene como primer carácter un # (carácter almohadilla).

A continuación vamos a describir brevemente algunas de las directivas de preprocesamiento más comúnmente usadas.

4.10.1 Directiva #define

La directiva #define, sirve para definir tanto constantes como macros. Esto suministra un sistema para la sustitución de palabras, con y sin parámetros.

Su sintaxis es la siguiente:

#define nombre_macro <secuencia>

El preprocesador sustituirá cada ocurrencia de nombre_macro en el fichero fuente, por la secuencia con algunas excepciones. Cada sustitución se conoce como una expansión de la macro. Si por alguna razón el campo <secuencia> no existiese, el nombre_macro será eliminado cada vez que aparezca en el fichero fuente.

Page 113: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 219

La expansión de las macros se realiza de forma recursiva, es decir, después de cada expansión individual, se vuelve a examinar el texto expandido a la búsqueda de nuevas macros, que serán expandidas a su vez. Esto permite la posibilidad de hacer macros anidadas.

Existen otras restricciones a la expansión de macros:

Las ocurrencias de macros dentro de literales, cadenas, constantes alfanuméricas o comentarios no serán expandidas.

Una macro no será expandida durante su propia expansión, así #define A A, no será expandida indefinidamente.

No es necesario añadir un punto y coma para terminar una directiva de preprocesador. Cualquier carácter que se encuentre en una secuencia de macro, incluido el punto y coma, aparecerá en la expansión de la macro. La secuencia termina en el primer retorno de línea encontrado. Las secuencias de espacios o comentarios en la secuencia, se expandirán como un único espacio.

4.10.2 Directiva #undef

Sirve para eliminar definiciones de macros previamente definidas. La definición de la macro se olvida y el identificador queda indefinido.

Su sintaxis es:

#undef nombre_macro

Después de que una macro quede indefinida puede ser definida de nuevo con #define, usando la misma u otra definición.

4.10.3 Directivas #if, #elif, #else y #endif

Permiten hacer una compilación condicional de un conjunto de líneas de código.

Su sintaxis es:

#if <condicion1> <seccion1> #elif <condicion2> <seccion2> . . . #elif <condicionN> <seccionN> <#else> <seccionFinal> #endif

Sólo se compilarán las líneas que estén dentro de las secciones que cumplan la condición de la expresión constante correspondiente.

Estas directivas funcionan de modo similar a los operadores condicionales C. Si el resultado de evaluar la <condicion1>, que puede ser una macro, es distinto de cero (true), las líneas representadas por sección1, ya sean líneas de comandos, macros o incluso nada, serán compiladas. En caso contrario, si el resultado de la evaluación de la <condicion1>, es cero (false), la <seccion1> será ignorada.

En todo caso, sólo se ejecutará una sección, así en caso de que la <condicion1> sea distinta de cero, se ejecutaría la <seccion1> y se pasaría al #endif. En caso de ser cero, pasaríamos a evaluar

Page 114: capitulo4

220 ♦ Capítulo 4: Fundamentos de Programación.

<condicion2>. Si ninguna condición se cumple, pasaríamos al #else (en caso de que existiera), para posteriormente terminar.

Cada sección procesada puede contener a su vez directivas condicionales, anidadas hasta cualquier nivel, cada #if debe corresponderse con el #endif más cercano.

4.10.4 Directivas #ifdef e #ifndef

Estas directivas permiten comprobar si un identificador está o no actualmente definido, es decir, si un #define ha sido previamente procesado para el identificador y si sigue definido.

Su sintaxis es:

#ifdef <identificador> #ifndef <identificador>

4.10.5 Directiva #error

Esta directiva se suele incluir en sentencias condicionales de preprocesador para detectar condiciones no deseadas durante la compilación. En un funcionamiento normal estas condiciones serán falsas, pero cuando la condición es verdadera, es preferible que el compilador muestre un mensaje de error y detenga la fase de compilación. Para hacer esto se debe introducir esta directiva en una sentencia condicional que detecte el caso no deseado.

Sintaxis:

#error mensaje_de_error

4.10.6 Directiva #include

La directiva #include sirve para insertar ficheros externos dentro de nuestro fichero de código fuente.

Su sintaxis es la siguiente:

#include <nombre de fichero cabecera> #include "nombre de fichero de cabecera" #include identificador_de_macro

El preprocesador elimina la línea #include y la sustituye por el fichero especificado. El tercer caso obtiene el nombre del fichero como resultado de aplicar la macro. El código fuente en si no cambia, pero el compilador "ve" el fichero incluido.

La diferencia entre escribir el nombre del fichero entre <> o "", está en el algoritmo usado para encontrar los ficheros a incluir. En el primer caso el preprocesador buscará en los directorios include definidos en el compilador. En el segundo, se buscará primero en el directorio actual, es decir, en el que se encuentre el fichero fuente, si no existe en ese directorio, se trabajará como el primer caso. Si se proporciona el camino como parte del nombre de fichero, sólo se buscará es el directorio especificado.

4.11 EJERCICIOS PROPUESTOS

1. ¿Qué ventajas y desventajas ofrecen los lenguajes de alto nivel frente al lenguaje máquina?

Page 115: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 221

2. ¿Cuál es la diferencia principal entre un lenguaje interpretado y uno compilado? Ponga algún ejemplo de cada uno de ellos.

3. Escriba un programa en C que, dado un número real introducido por teclado, imprima dicho número de las siguientes formas:

Solo su parte entera. Su parte entera y dos decimales. El número completo. Sólo su parte decimal (la parte entera debe presentarse como un 0).

4. ¿Qué valor tiene la variable a al terminar cada uno de los siguientes fragmentos de código?

a = a & 1;

if ( !(a & 1) ) a++; a = a | 1 ;

5. ¿Qué valor tienen las variables x e y al terminar cada uno de los siguientes fragmentos de código?

int y=4, x;

x = y++%2-1;

int y=4, x;

x = ++y % 2-1;

int y=4, x;

x = --y/2-1;

int y=4, x;

x = y--/2-3/2;

6. Usando las leyes de De Morgan, escriba las condiciones contrarias de cada sentencia if:

if ( x-1<3 && y++>x+2 )

if ( x/2==3 || ++x>=0 )

if ( x-1 && y%2 )

7. Escriba un programa en C que, dada una fecha introducida por teclado (día y mes) correspondiente al año 2003, imprima por pantalla la estación del año a la que pertenece esa fecha. Las fechas de inicio de las estaciones en este año son:

Estación Fecha de inicio (día/mes) Primavera 21/3

Verano 21/6 Otoño 23/9

Invierno 22/12

8. Escriba un programa en C que, dada una fecha introducida por teclado (día y mes) correspondiente al año 2003, imprima por pantalla el día de la semana en que cae la fecha introducida, sabiendo que el 1 de Enero de 2003 fue miércoles.

9. Escriba un programa en C que, dado un punto de coordenadas x e y, indique si está contenido en un círculo de radio r cuyo centro está en el origen de coordenadas. Los valores x, y y r se introducen por teclado. (Nota: La ecuación de una circunferencia de radio r centrada en el origen es: x2 + y2 = R2).

Page 116: capitulo4

222 ♦ Capítulo 4: Fundamentos de Programación.

10. Escriba un programa que lea un carácter por teclado y muestre por pantalla uno de los caracteres que se indican en la tabla, en función del carácter leído. Si el carácter leído no está en la tabla, mostrará el carácter cero. Utilice la sentencia switch.

Carácter leído Carácter en pantalla 1 A 2 B 3 C 4 D

Otro carácter 0

11. Escriba un programa que pida por teclado un valor numérico, compruebe que es de tres cifras (en base decimal), y si es así imprima por pantalla las unidades, decenas y centenas.

12. Escriba un programa que pase de segundos a horas, minutos y segundos (con centésimas de segundo) y otro que haga el cambio contrario.

13. Realice un programa que pida por teclado un valor numérico con decimales, correspondiente a la calificación de un examen, y que indique por pantalla si se corresponde con un suspenso, aprobado, notable o sobresaliente. Si la nota tiene un valor incorrecto, debe mostrar un mensaje de error. El programa debe realizarse utilizando la sentencia switch para distinguir entre los diferentes valores de la calificación. Pruebe el programa con los siguientes valores: 4.99, 7, 10.1, 13, -4, -0.5

14. Sea una variable c de tipo char. Responda a las siguientes cuestiones:

Suponiendo que c tiene un valor entre ‘0’ y ‘9’, escriba una expresión que dé como resultado un número entero entre 0 y 9.

Suponiendo que c tiene un valor entre ‘0’ y ‘9’, escriba una expresión que dé como resultado un carácter entre ‘A’ (para c igual a ‘0’) y ‘J’ (para c igual a ‘9’).

Suponiendo que c tiene un valor entre ‘A’ y ‘Z’, escriba una expresión que dé como resultado un número entero entre 0 (para c igual a ‘A’) y 25 (para c igual a ‘Z’).

Suponiendo que c tiene un valor entre ‘A’ y ‘Z’, escriba una expresión que dé como resultado la correspondiente letra minúscula.

Suponiendo que c tiene un valor entre ‘a’ y ‘z’, escriba una expresión que dé como resultado la correspondiente letra mayúscula.

15. ¿Qué valor tiene la variable condicion (de tipo int) al terminar los siguientes fragmentos de código?

condicion = var % 2 == 0;

condicion = var < 10;

condicion = condicion || var > 100;

condicion = !condicion;

16. Utilizando solamente dos variables de tipo int y una sentencia if, escriba un programa que lea tres números enteros del teclado y que imprima un mensaje por pantalla si todos pertenecen al intervalo [0,10].

17. ¿Cuántas iteraciones del bloque se realizan al ejecutar los siguientes bucles?:

for ( i=0 ; i <100 ; i=i+2 ) { /* bloque */ };

for ( i=0 ; i <100 ; i-- ) { /* bloque */ };

for ( i=100 ; i>0 ; i-- ) { /* bloque */ };

for ( i=0 ; i<100 ; i=i+2 ) {

for ( j=0 ; j<100 ; j=j+2 ) { /* bloque*/ };}

for ( i=0 ; i<100 ; i++ ) {

Page 117: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 223

for ( j=0 ; j<i ; j++ ) { /* bloque */ };}

18. Escriba un programa en C que imprima los 45 primeros términos de la sucesión de Fibonacci por pantalla. Los primeros términos de dicha sucesión son: 1, 2, 3, 5, 8, ... El término n de la sucesión (para n>=3) viene dado por la suma de los dos términos inmediatamente anteriores a él:

321

21

2

1

≥∀+===

−− nxxxxx

nnn

19. Escriba un programa en C que pida un número entre 1 y 10 por teclado, y a continuación imprima la tabla de multiplicar de dicho número en la pantalla.

20. Escriba un programa en C que permita a dos jugadores jugar a adivinar un número. El programa pide al primer jugador que introduzca un número entero entre 1 y 100. Después, el programa imprime 50 veces un “\n” (para borrar la pantalla y que no se vea el número). A partir de ahí, el programa irá preguntando al segundo jugador qué número ha pensado el primero. Cuando el segundo jugador introduzca un número, el programa debe responder con “Demasiado bajo, pruebe de nuevo.”, “Demasiado alto, pruebe de nuevo” o “Acertaste!!” según sea el número introducido. El juego acaba cuando el segundo jugador acierta el número.

21. Escriba un programa en C que implemente una pequeña calculadora en pantalla con las siguientes opciones: Introducir operandos, sumar, restar, multiplicar, dividir, convertir a euros, y convertir a pesetas. Las cuatro primeras operaciones (suma, resta, producto y división) se harán con los operandos introducidos por teclado en la primera opción del menú. Las dos últimas operaciones (las conversiones de moneda) se harán tomando como argumento el resultado de la última operación realizada, o el valor del primer operando, si no se realizó ninguna operación. El menú tendrá una opción más para salir del programa.

22. Escriba un bucle que calcule la potenciación de grado n (positivo) de la base x.

23. Escriba un bucle que calcule la potenciación de grado n (entero con signo) de la base x.

24. Escriba un bucle infinito (del que nunca saldrá el programa) con un while, for y do while, de la forma más simple posible.

25. Usando la llamada regla de Simpson, escriba una aproximación mejor que la vista en el texto de la integral definida de una función. La fórmula de la regla de Simpson es:

≈∫b

adxxf )( [ ]( )∑

=

∆+∆++∆+1

0

*2/))1(*()*(N

i

xixafixaf

Se supone que f(x)=x3+2x

26. Escriba un bucle que no haga nada y calcule el número de iteraciones para que dure un segundo (use un reloj externo).

27. Considere el bucle anterior y haga un reloj digital con horas, minutos y segundos.

28. Realice un programa que indique si un número impar es primo (imprima por pantalla “S”) o no (imprima por pantalla “N”), comprobando si es divisible por los impares inferiores a él.

29. Imprima por pantalla la descomposición factorial de un número. (Nota: compruebe primero si el número es divisible por 2; si lo es, imprimir un 2 y seguir iterando para buscar factores pero con el numero=numero/2. Cuando se hayan sacado todas las veces que es divisible por 2, repetir todo el proceso para los números impares).

Page 118: capitulo4

224 ♦ Capítulo 4: Fundamentos de Programación.

30. Haga un programa que calcule las combinaciones de n elementos tomados de k en k. Considere la siguiente fórmula matemática:

!)1)...(2)(1(

kknnnnC

kn k

n+−−−

==

31. Una pelota rebota en el suelo tras caer desde cierta altura H y con cierta velocidad inicial v inic . Calcule su posición tras n rebotes y t1 segundos tras el último rebote. Datos:

En cada rebote la velocidad se amortigua en k veces, pasando de v a –kv.

La posición de la pelota viene por la fórmula 200 5.0 gttvyy ++= , donde v0 es la velocidad

inicial y g=9.81 m/s. La velocidad de una pelota al llegar al suelo, tras caer desde una altura H y con cierta velocidad

inicial v0, viene dada por la fórmula:

gHvv 220 +=

Tras cada rebote, la velocidad con la que la pelota llega de nuevo al suelo es igual a la velocidad con la que partió hacia arriba.

32. Escriba un programa en C que, dada una fecha introducida por teclado (día y mes) correspondiente al año 2003, imprima por pantalla el número de días que han transcurrido desde el 1 de enero de 2003 hasta la fecha dada, ambos inclusive.

33. El siguiente programa indica si un número leído desde la entrada estándar es par o impar:

#include <stdio.h> void main (void) { int numero; scanf ( "%d", &numero ); if ( numero%2 == 0 ) printf ( "Es un numero par" ); else printf ( "Es un numero impar" ); }

Convierta el programa en una función que reciba como parámetro el número y devuelva como resultado un 1 si es par o un 0 si es impar. Utilice esta función en un programa que pida por teclado un número entero y devuelva por pantalla el resultado.

34. Escriba una función que tenga un único parámetro que sea tipo carácter, y devuelva 1 si el carácter está en mayúsculas, -1 si está en minúsculas o 0 si no es una letra.

35. Escriba una función que reciba como entrada un carácter y devuelva otro. Si el carácter de entrada es una letra mayúscula deberá retornar la correspondiente en minúscula y viceversa. Si el carácter no fuera una letra se devolverá el mismo carácter. Compruebe que para ‘d’, ‘D’ y ‘7’ la función devuelve ‘D’, ‘d’ y ‘7’ respectivamente.

36. Desarrolle una función que reciba como parámetros las dos coordenadas cartesianas en doble precisión (x, y) de un punto del plano y devuelva como resultado un número del 1 al 4 que indique el cuadrante al cual pertenece al punto (o un 0 si el punto está en uno de los ejes de coordenadas).

37. El siguiente programa calcula la potencia n-ésima de un número entero x, xn:

Page 119: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 225

#include <stdio.h> void main (void) { int n, x ; double x, potencia; printf ( "\nIntroduzca la base y el exponente: " ); scanf ( "%lf%d", &x, &n ); potencia = 1; for ( i=0; i<n; i++ ) potencia = potencia * x; printf ( "\nLa potencia es %lf", potencia ); }

Modifíquelo transformándolo en una función que reciba los valores de x y n como parámetros de entrada y devuelva el resultado como salida.

38. Escriba una función que devuelva el menor divisor (mayor que 1) de un número entero positivo.

39. Escriba otra función que, llamando a la función anterior, devuelva si un número es primo o no.

(Nota: solamente compruebe que el menor divisor coincide o no con el propio número).

40. Escriba una función que reciba como parámetro de entrada un número entero y devuelva como resultado el número de cifras del número en base decimal. (Nota: divida por 10 repetidas veces hasta que quede 0).

Para ello, considere lo siguiente: dado un número cualquiera, por ejemplo el 173, se tiene que 173/10=17 (todas las cifras salvo la última). Hay que repetir este proceso hasta que el número resultante de la división tenga una sola cifra. A continuación utilice esta función en un programa que pida por teclado un número decimal y devuelva el número de cifras por pantalla.

41. El siguiente programa simula el pedido de un par de productos en una droguería y el cálculo del coste total del pedido en euros. Para ello se utiliza la función calcular_precio que nos hace el cálculo del coste para un producto. Sin embargo, el coste total del pedido que es devuelto por el programa no es correcto. Modifique el programa para que devuelva costes totales correctos.

Page 120: capitulo4

226 ♦ Capítulo 4: Fundamentos de Programación.

#define IVA 1.16f #define coste_litro_pintura 1.50f #define coste_litro_aguarras 2.50f #define SI 1 #define NO 0 #include <stdio.h> float calcular_precio (int, float, int); float total=0.0; void main() { int cantidad; /* Se pregunta al usuario por los litros de pintura que quiere comprar */ printf ( "Indique cuantos litros de pintura subvencionada quiere comprar:" ); scanf ( "%d", &cantidad ); /* Se calcula el coste de los litros de pintura y se suma al coste total acumulado */ total = total + calcular_precio (cantidad, coste_litro_pintura, NO); /* Se pregunta al usuario por los litros de aguarras que quiere comprar */ printf ( "\n\nIndique cuantos litros de aguarras quiere comprar: "); scanf ( "%d", &cantidad ); /* Se calcula el coste de los litros de aguarras y se suma al coste total acumulado */ total = total +calcular_precio (cantidad, coste_litro_aguarras,SI); /* Se devuelve el coste total acumulado */ printf ( "El coste total es: %.2f \n", total ); } float calcular_precio (int cantidad, float coste_unidad, int poner_iva) /* Funcion que calcula el precio del pedido solicitado */ { total = cantidad*coste_unidad; if ( poner_iva==SI ) total = total*IVA; return total; }

42. El siguiente programa calcula superficies de diversas formas geométricas. Inicialmente muestra un menú inicial con las diferentes opciones posibles. Estas opciones permiten calcular el área de ciertas figuras geométricas. A continuación el programa lee la opción (número) introducida por el usuario y pide los parámetros necesarios para calcular el área de la figura geométrica seleccionada (radio, base, altura, ...). Finalmente devuelve el resultado calculado y vuelve a mostrar el menú inicial. Se pide que se modularice este programa, es decir, que se le divida en una función principal y al menos tres funciones secundarias.

Page 121: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 227

#include<stdio.h> #define PI 3.1415926 #define REC 1 #define TRA 2 #define CIR 3 #define SALIR 0 #define NOSALIR 1 main() { int opcion=NOSALIR; float base, altura, radio, base1, base2; double area; while ( opcion!=SALIR ) { /* Se muestra el menu de inicio del programa */ printf ( "\n \n \n \n \n \n \n \n \n \n" ); printf("===========================================\n" ); printf ( "CALCULO DE SUPERFICIES (version 1.0) \n" ); printf("===========================================\n" ); printf ( "1. Rectangulo \n" ); printf ( "2. Trapecio \n" ); printf ( "3. Circulo \n" ); printf ( "0. SALIR \n" ); printf("===========================================\n" ); scanf ( "%d", &opcion ); /* En funcion de la opcion elegida se calcula un area u otro*/ if ( opcion==REC ) { printf ( "\nIntroduzca la base del rectangulo: " ); scanf ( "%f", &base ); printf ( "Introduzca la altura del rectangulo: " ); scanf ( "%f", &altura ); area = base*altura; } else if ( opcion==TRA ) { printf("\nIntroduzca la base1 (mayor) del trapecio:" ); scanf ( "%f", &base1 ); printf("Introduzca la base2 (menor) del trapecio: " ); scanf ( "%f", &base2 ); printf ( "Introduzca la altura del trapecio: " ); scanf ( "%f", &altura ); area = (base1+base2)*altura/2.0; } else if ( opcion==CIR ) { printf ( "\nIntroduzca el radio del circulo: " ); scanf ( "%f", &radio ); area = PI*radio*radio; } else { /* Error: valor no valido. Se decide salir del programa */ opcion = SALIR; } /* Se muestra el resultado obtenido */ switch ( opcion ) { case REC: printf ( "\t El area del RECTANGULO es: %.2f \n", area ); break; case TRA: printf ( "\t El area del TRAPECIO es: %.2f \n", area ); break; case CIR: printf ( "\t El area del CIRCULO es: %.2f \n", area ); } } }

Page 122: capitulo4

228 ♦ Capítulo 4: Fundamentos de Programación.

43. Escriba un programa que permita llevar los gastos e ingresos menores de un millón de pesetas de una empresa. El programa recoge estos datos desde la entrada estándar y presenta un menú como el siguiente:

================================= CONTROL DE GASTOS (version 1.0) 1. Ingresos 2. Gastos 3. Salida

El diagrama de bloques es el siguiente:

Programa

menu lee_opcion ingresos gastos salida

Donde las funciones realizan las siguientes operaciones:

menu() muestra el menú en la pantalla. lee_opcion() lee la opción del teclado. ingresos() lee nuevos ingresos y los suma al saldo actual. gastos() lee nuevos gastos y los resta del saldo actual (que nunca podrá ser negativo). salida() muestra el saldo actual (ingresos - gastos).

44. El siguiente programa calcula el doble y el cuadrado de los 10 primeros números naturales. Los almacena en dos vectores y luego imprime sus contenidos por pantalla. Se compila sin dar errores, pero ¿por qué no funciona? Modifíquelo para que funcione sin problemas.

#include <stdio.h> #define TAM 10 void main(void) { int t1[TAM],t2[TAM]; int i; for ( i=0;i<=TAM;i++ ) { t1[i]=2*i; t2[i]=i*i; } for ( i=0;i<=TAM;i++ ) printf("\n%d %d\n",t1[i],t2[i]); }

45. Escriba un programa que lea una lista de números enteros positivos, los almacene en una tabla y los imprima en orden inverso al que fueron leídos. El tamaño de la lista es desconocido, siendo 50 como máximo. El final de la lectura ocurrirá cuando se hayan leído 50 números o se lea un valor negativo o cero.

46. Escriba un programa que realice, mediante estructuras repetitivas, el recorrido en diagonal de la matriz cuadrada m de NxN elementos empezando en el extremo inferior izquierdo y finalizando en el último elemento de la diagonal principal. Por ejemplo, para la siguiente matriz

Page 123: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 229

987654321

debe presentarse en pantalla 7 4 8 1 5 9. El programa lee la matriz del teclado e imprime el resultado del recorrido diagonal propuesto.

47. La función intercambia() pretende intercambiar los valores de sus dos parámetros.

#include <stdio.h> void intercambia (int, int); void main (void) { int a=3, b=5; printf ("\na vale %d y b vale %d\n", a, b); intercambia (a, b); printf ("\na vale %d y b vale %d\n", a, b); } void intercambia (int x, int y) { int temp; temp = x; x = y; y = temp; }

¿Realiza la función su cometido? En caso contrario, modifíquela adecuadamente para que lo haga.

48. Escriba la función:

void maxmin (int x1, int x2, int *max, int *min);

que reciba como parámetros de entrada dos números enteros x1 y x2 y devuelva a través de los parámetros de salida max y min el máximo y el mínimo, respectivamente, de ambos números.

49. La función

int valida_dato (int dato, int min, int max, int defecto) { int resul=defecto; if ( dato >= min && dato <= max ) resul = dato; return (resul); }

comprueba si el parámetro de entrada dato se encuentra comprendido entre los dos límites min y max. En caso afirmativo, devuelve el propio valor dato como resultado, mientras que en caso negativo devuelve el valor del parámetro defecto. Modifique esta función para que adopte el siguiente prototipo:

void valida_dato (int *dato, int min, int max, int defecto);

es decir, que dato pase a ser un parámetro por referencia (de entrada y salida).

50. Realice una función

Page 124: capitulo4

230 ♦ Capítulo 4: Fundamentos de Programación.

void siguiente_fecha (int *dia, int *mes, int *anyo);

que reciba a través de los parámetros dia, mes y anyo una fecha válida y devuelva a través de los mismos la siguiente fecha válida del calendario. Por ejemplo,

Entrada Salida 31 1 1996 1 2 1996 28 2 1996 29 2 1996 31 12 1996 1 1 1997 2 1 1997 3 1 1997

51. Escriba la función void tabla_multiplos( int n, int multiplos[10] ), que reciba como entrada un número entero y construya una tabla con los 10 primeros múltiplos de dicho número. A continuación utilice esa función en un programa que pida por teclado un número e imprima por pantalla sus 10 primeros múltiplos.

52. Usando la función anterior, escriba un programa que declare una tabla de 10x10 elementos de tipo entero y la rellene de la siguiente forma:

1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 ... 10 20 30 40 50 60 70 80 90 100

53. Realice una función que escriba en la pantalla los elementos de una tabla unidimensional que cumplen una determinada condición. Para ello se tiene la siguiente declaración:

void procesa_tabla (const int t[], int n, int operador, int valor);

donde

t es la tabla a procesar. n es el número de elementos de la tabla. operador es el tipo de comparación a efectuar, y puede tomar los valores constantes MAYOR,

MENOR o IGUAL (constantes definidas). valor es el valor con el cual hay que comparar.

Por ejemplo, la llamada

procesa_tabla (tab, n, MAYOR, 0);

escribe los elementos de la tabla tab que son mayores que cero.

54. Implemente las siguientes funciones para copiar y concatenar subcadenas:

Page 125: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 231

char *copia_subcad( char *destino, char* fuente, unsigned desde, unsigned hasta );

char *concatena_subcad( char *destino, char* fuente, unsigned desde,

unsigned hasta );

La función copia_subcad() debe copiar, en la cadena de caracteres destino, la porción de la cadena fuente que comienza en la posición desde y termina en la posición hasta-1. La función concatena_subcad() añade la porción seleccionada de la cadena fuente al final de la cadena destino. Ambas funciones devuelven la cadena destino que se les ha pasado como argumento. Pruebe las funciones ejecutando el siguiente programa:

void main() { char cadena1[100]; char cadena2[100] = "esto es una prueba"; printf( "%s\n", concatena_subcad( copia_subcad( cadena1,

cadena2, strlen(cadena2)/2, strlen(cadena2) ), cadena2, 0, strlen(cadena2)/2 ) ); }

55. Implemente la siguiente función, que realiza la misma operación que la función estándar strcmp():

int compara_cadenas( char *cad1, char *cad2 );

56. Implemente la siguiente función:

unsigned copia_palabra( char destino[], char fuente[], unsigned posicion );

Dicha función busca una palabra (una secuencia de caracteres separada por uno o más espacios) en la cadena de caracteres fuente y la copia en la cadena destino. La búsqueda comienza en la posición indicada por el parámetro posicion. Devuelve la posición de la cadena fuente donde termina la palabra encontrada, o cero si no ha encontrado ninguna. Por ejemplo, la siguiente llamada a la función:

copia_palabra( cad, “esto es una prueba”, 4 );

comienza la búsqueda en la posición 4, que corresponde al primer espacio; almacena la cadena “es” en cad; y devuelve el valor 7, que corresponde a la posición del segundo espacio. Pruebe la función con una cadena que contenga varias palabras separadas por más de un espacio consecutivo y que contenga espacios al principio y al final de la cadena.

57. Utilice las funciones copia_palabra() y compara_cadena() de los ejercicios anteriores y realice una función que reciba como parámetro una cadena de caracteres, busque la primera y última palabra de la cadena en orden alfabético, y las muestre por pantalla. Suponga que la longitud máxima de una palabra no supera los 80 caracteres y que no puede existir más de 20 palabras en una cadena.

58. Escriba la siguiente función:

unsigned busca_subcadena( char cad[], unsigned pos, char subcad[]);

dicha función busca la cadena de caracteres subcad dentro de la cadena cad, comenzando la búsqueda en la posición pos. Si la encuentra, devuelve su posición dentro de cad; en caso contrario, devuelve la posición del carácter nulo que marca el final de cad (esto es equivalente a la longitud de cad). Por ejemplo: la llamada a la función,

Page 126: capitulo4

232 ♦ Capítulo 4: Fundamentos de Programación.

busca_subcadena( “esto es una prueba”, 0, “es” );

busca la cadena “es” a partir de la posición 0 y devuelve 0, que corresponde a la posición de “esto ...”. La llamada a la función,

busca_subcadena( “esto es una prueba”, 1, “es” );

devuelve 5, que corresponde a la posición de “esto es ...”. La llamada a la función,

busca_subcadena( “esto es una prueba”, 6, “es” );

devuelve 18, que corresponde a la longitud de la cadena.

59. Utilice las funciones busca_subcadena() y concatena_subcad() y escriba una función que realice búsquedas y sustituciones dentro de una cadena de caracteres. El prototipo es el siguiente:

char *busca_sustituye( char *destino, char *origen, char *cad_busca, char *cad_sust );

donde, origen es la cadena original; y destino es una copia de la cadena origen, en la que se han sustituido todas las ocurrencias de la cadena cad_busca por cad_sust. La función devuelve la cadena destino que se le ha pasado como parámetro.

Pruebe la función ejecutando el siguiente programa:

void main() { char cadena[100] = "este estado ees inestable"; char resultado[100]; printf( "%s\n", cadena ); printf( "%s\n", busca_sustituye( resultado, cadena, "es",

"Esss" ) ); /* Debe imprimir por pantalla: Essste Essstado eEsss inEssstable */ }

60. Realice un programa que genere dos ficheros de texto que contengan números, uno de ellos contendrá números pares y otro impares empezando desde cero. El límite de la secuencia de números es introducido por el usuario a través del teclado.

61. Desarrolle un programa en C que copie el contenido de un fichero secuencial de texto en otro. Los nombres del fichero de entrada y de salida son introducidos por el usuario mediante teclado, y han de estar en el directorio de la aplicación por defecto.

62. Una empresa almacena los datos de sus clientes en un fichero de texto llamado “clientes.txt”. Cada registro del fichero está compuesto por los siguientes campos: el nombre del cliente (cadena de 80 caracteres), la dirección del cliente (cadena de 80 caracteres), y el número de zona comercial (entero del 1 al 5). Todos los registros terminan con el carácter de fin de línea (‘\n’).

Escriba un programa que lea del teclado los datos del cliente y los almacene en el fichero de clientes.

63. Escriba un programa que lea el fichero de clientes del ejercicio anterior (clientes.txt), y genere un nuevo fichero de cliente para cada una de las zonas comerciales. El fichero de clientes “clientes1.txt” contendrá los clientes de la zona 1; el fichero “clientes2.txt”, los clientes de la zona2, y así sucesivamente.

Page 127: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 233

64. Realice un programa que genere un tercer fichero ordenado, a partir de otros dos, formados por números enteros ordenados.

65. Se tienen dos ficheros de texto F1 y F2. Ambos se han usado para codificar un mensaje. Se quiere reconstruir el mensaje original, es decir, decodificar el mensaje inicial a partir de estos dos ficheros. El fichero F1 tiene todos los caracteres impares del mensaje original y el fichero F2 todos los caracteres pares del mensaje original (incluyendo caracteres en blanco y signos de puntuación). El mensaje final decodificado se ha de guardar en un tercer fichero F3. A continuación se muestra un ejemplo de decodificación de un mensaje:

Fichero F3: hola que tal Fichero F1: hl u tl

Fichero F2: oaqea

66. Escriba un programa en C que lea un archivo de texto denominado “entrada.txt” y genere otro denominado “salida.txt” que contenga la(s) letra(s) que aparecen con mayor y menor asiduidad en el archivo de entrada.

67. Utilizando la función busca_subcadena(), propuesta en el ejercicio 56 del apartado 4, escriba una función que busque una cadena en un fichero de texto y muestre por pantalla las líneas de texto en las que aparece dicha cadena, precedidas del número de línea. El prototipo es el siguiente:

int busca_texto( FILE* fichero, char *cadena );

donde, los parámetros cadena y fichero son la cadena de caracteres a buscar y el fichero de texto, respectivamente. La función devuelve el número total de líneas en las que se ha encontrado la cadena. Por simplicidad, puede considerarse que el fichero de texto no va a tener líneas de más de 500 caracteres. Escriba un programa que pida por el teclado el nombre del fichero y la cadena a buscar, y que ejecute la función anterior. El programa debe mostrar por pantalla el número de líneas que contienen la cadena.

68. Utilizando las funciones busca_subcadena() y copia_subcad(), propuestas en los ejercicios 52 y 56 del apartado 4, escriba una función que realice la búsqueda y sustitución de una cadena en un fichero de texto, guardando el resultado en un fichero de salida. El prototipo es el siguiente:

int sustituye_texto( FILE *fuente, FILE *destino, char *cad_busca, char *cad_sust );

donde, fuente es el fichero de entrada, destino es el fichero de salida, cad_busca es la cadena que debe buscarse, y cad_sust es la cadena que reemplazará a cad_busca. La función devuelve el número de sustituciones que se han realizado en el fichero de salida. Por simplicidad, considere que el fichero de texto no tiene líneas de más de 500 caracteres. Escriba un programa que pida por el teclado el nombre del fichero de entrada, la cadena de búsqueda y la cadena de sustitución. El programa debe realizar la búsqueda y sustitución, guardar el resultado en el fichero “salida.txt”, y mostrar por pantalla el número de sustituciones que se han realizado.

69. Una determinada empresa almacena las ventas del mes en un archivo denominado “ventas.txt” y las devoluciones en uno llamado “devoluciones.txt”. Ambos tienen la estructura descrita a continuación:

Campos:

Día (entero 2, dígitos) Mes (entero 2, dígitos) Año (entero 4, dígitos) Código artículo (entero, 6 dígitos) Descripción artículo (ASCII, 80 caracteres) Zona (un dígito, del 1 al 5)

Page 128: capitulo4

234 ♦ Capítulo 4: Fundamentos de Programación.

Importe (6 dígitos y 2 decimales)

Los registros están ordenados por el campo código de artículo y todos los campos numéricos van seguidos de un espacio. Como las ventas son mensuales los campos mes y año mantienen siempre el mismo valor dentro del fichero.

Ejemplo:

21 11 2003 432989 Mesa escritorio 3 120.57 25 11 2003 432989 Mesa escritorio 2 120.57 18 11 2003 433001 Silla escritorio 1 65.23 99 99 9999 999999 XXXXXXXXXXXXXXXXX...XX9 999999.99

Genere dos nuevos ficheros de texto a partir de los ficheros descritos. Uno llamado “total_cod.txt” que contenga exclusivamente el total de ventas y el total de devoluciones del mes por cada código de articulo. Y otro denominado “total_zona.txt” que contenga por cada zona y día del mes el total de ventas y el total de devoluciones.

70. Defina los siguientes tipos de estructuras:

Un tipo estructura Hora con tres campos enteros: hora, minutos, segundos. Un tipo estructura Fecha con tres campos enteros: dia, mes, anyo. Un tipo estructura Persona con los siguientes campos:

nombre: cadena de 20 caracteres alfanuméricos. apellidos: cadena de 40 caracteres alfanuméricos. dni: entero largo sin signo. fecha_nacimiento: estructura de tipo Fecha.

Un tipo estructura EntradaDir con los siguientes campos: nombre: cadena de 8 caracteres alfanuméricos. extension: cadena de 3 caracteres alfanuméricos. tamanyo: entero largo sin signo. fecha_creacion: estructura de tipo Fecha. hora_creacion: estructura de tipo Hora.

71. Si se supone que en una plataforma determinada un entero son 4 bytes,

¿Cuál es la longitud de la variable q en bytes? Utilizando sizeof, ¿cómo se puede calcular el valor que tiene asignado TAM? struct elemento { int x; int y; struct { int x; int y; int vec [4]; } p; } q; struct elemento vector[TAM];

72. Dada la siguiente estructura:

Page 129: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 235

struct registro { char a; int b; double c; };

Se tiene una función llamada visualiza() que imprime por pantalla su contenido, cuyo prototipo es el siguiente:

void visualiza (struct registro *val);

Implemente la función anterior y utilícela en un programa que declara una variable de tipo registro y la inicializa con los siguientes valores: a←’N’, b← 5, c← 5.5

73. Un número complejo se puede representar mediante la siguiente estructura:

typedef struct { double a; /* Parte real */ double b; /* Parte imaginaria */ } Complejo;

Escriba las siguientes funciones de manipulación de números complejos: A partir de la parte real y la parte imaginaria, crea y devuelve como resultado un número

complejo: Complejo crea_cplj( double real, double imag ); Imprime por pantalla el parámetro de tipo complejo en la notación a+b i : void print_cplj( Complejo c ); Lee un dato de tipo complejo del teclado y lo devuelve: Complejo lee_cplj( void ); Lee un dato de tipo complejo del teclado y lo escribe en la variable por referencia, apuntada

por el parámetro tipo puntero (similar a la función scanf()): void scan_cplj( Complejo *c ); Devuelve la suma de los dos parámetros de tipo complejo: Complejo suma_cplj( Complejo c1, Complejo c2 ); Devuelve el producto de los dos parámetros de tipo complejo: Complejo mult_cplj( Complejo c1, Complejo c2 ); Utilizando las funciones anteriores, escriba un programa que lea un número complejo del

teclado y lo utilice para calcular el valor del siguiente polinomio de variable compleja:

154 2 +⋅−⋅ xx

74. El siguiente programa lee desde la entrada estándar el nombre y los puntos de los 20 equipos de una liga de fútbol. Los nombres tienen un máximo de 22 caracteres. Corrija, justificadamente, los errores del programa.

Page 130: capitulo4

236 ♦ Capítulo 4: Fundamentos de Programación.

#include <stdio.h> #define MAX 22 ; typedef char Nombre[MAX]; typedef strutc { char Nombre; int puntos; } Equipo; void main (void) { Equipo liga[]; for (i=0; i<=MAX; i++) scanf ("%s%d", liga[i].NOMBRE, liga[i].puntos); }

75. Sean las siguientes definiciones:

#define MAXCAR 31 typedef struct { char nombre[MAXCAR], apellido1[MAXCAR], apellido2[MAXCAR]; int edad; long dni; char sexo; } Persona

Escriba una función:

int busca_persona (Persona clase [], int npersonas, long dni);

que busque en los primeros npersonas elementos de la tabla clase aquella persona cuyo DNI coincida con el valor especificado en el parámetro dni. La función devolverá la posición de la persona, si la encuentra, o un valor -1 en caso contrario.

76. Una determinada empresa quiere crear una base de datos de usuarios. Para identificar a uno de ellos se requiere el Documento Nacional de Identidad (DNI), el pasaporte, o en su defecto una clave interna asignada por la empresa. Cada usuario se identificará exclusivamente por uno de las tres formas (por ejemplo, el que se identifique con el DNI no tendrá asociado ni pasaporte ni clave interna). El DNI es un número entero positivo de 8 dígitos, el pasaporte una letra seguida de un numero entero positivo de 6 dígitos y la clave 4 caracteres alfanuméricos. Para cada usuario también se debe almacenar un tipo de estructura ya definida llamada DATOS_PERSONALES que contiene información de interés.

Defina la estructura menos costosa en memoria para la base de datos, teniendo en cuenta que van a ser almacenados hasta 100000 usuarios, y proponga una función que muestre el índice que ocupa un usuario en la base de datos a partir de su DNI, pasaporte o clave. Si el usuario no existe la función devolverá –1. El tipo de identificación utilizada por el usuario debe ser un tipo enumerado.

La definición de la función es:

int buscar(USUARIO* base, int num); /* USUARIO es la estructura que se pide */ /* num contiene el numero de elementos de la base de datos (tabla de usuarios)*/

77. Ponga algunos ejemplos de propiedades y eventos asociados que debería tener un componente botón en una herramienta de programación visual.

Page 131: capitulo4

Capítulo 4: Fundamentos de Programación. ♦ 237

78. Utilizando los tipos de datos y las funciones del ejercicio 71 del apartado 4, escriba un módulo para el tratamiento de números complejos y utilícelo para escribir el programa que se indica en dicho ejercicio. Debe escribir los siguientes ficheros:

“complejos.c” y “complejos.h”: ficheros que componen el módulo de tratamiento de números complejos.

“polinomio.c”: programa para el cálculo del polinomio de variable compleja.

79. Escriba un fichero Makefile para compilar los ficheros de código del ejercicio anterior. Debe definir dos reglas: una para compilar el módulo y otra para compilar el programa principal (que debe llamarse “polinomio.exe” ).

80. Supongamos que tenemos instalados dos compiladores de C en nuestro computador, llamados gcc y lcc. Modifique el fichero Makefile anterior para que, cambiando solamente una línea del fichero, se pueda especificar el compilador que queremos utilizar. Debe seguir existiendo el mismo número de reglas que en el Makefile original.

81. Escriba un módulo para el manejo de fechas con los siguientes tipos de datos y funciones:

Las fechas se representan por el tipo Fecha: typedef struct { unsigned int dia, mes, anyo; } Fecha; La variable pública formato_fecha, del tipo enumerado Formato_fecha, indica como

deben imprimirse las fechas por pantalla (ver tabla):

Valor de formato_fecha Ejemplo de impresión DDMMAAAA 23/11/2003

DIA_MES_ANYO 23 de noviembre de 2003 Otro valor EE/EE/EE

enum Formato_fecha { DDMMAAAA, DIA_MES_ANYO }; enum Formato_fecha formato_fecha; La variable pública error_fecha, del tipo enumerado Error_fecha, es actualizada por

ciertas funciones para indicar si la fecha que se está manipulando es correcta o no. enum Error_fecha { OK, ERROR_DIA, ERROR_MES, ERROR_ANYO }; enum Error_fecha error_fecha; La función crea_fecha() devuelve una fecha con el día, mes y año indicados en los

parámetros. Si la fecha es errónea actualiza la variable error_fecha. Fecha crea_fecha( unsigned int dia, unsigned int mes, unsigned

int anyo ); La función scan_fecha() lee una fecha por teclado y asigna los valores a la fecha que se le

pasa como parámetro. La función lee tres números que son el día, mes y año de la fecha. Esta función modifica la variable error_fecha.

int scan_fecha( Fecha *fecha ); La función chequea_fecha() comprueba si la fecha que se le pasa como parámetro es

correcta. Modifica la variable error_fecha. int chequea_fecha( Fecha fecha ); La función imprime_fecha() imprime por pantalla la fecha que se le pasa como

argumento, utilizando el formato indicado por la variable pública formato_fecha.

Page 132: capitulo4

238 ♦ Capítulo 4: Fundamentos de Programación.

void imprime_fecha( Fecha fecha ); La función compara_fechas() recibe dos fechas como parámetros, y las compara en

orden cronológico. Sea el siguiente prototipo: int compara_fechas( Fecha f1, Fecha f2 );

La función devuelve los siguientes valores:

Comparación de f1 y f2 Valor devuelto f1 < f2 un valor negativo (< 0)

f1 == f2 cero (== 0)

f1 > f2 un valor positivo (> 0)

El módulo debe utilizar la función privada es_bisiesto, que recibe un año como parámetro, y

devuelve VERDADERO si el año es bisiesto, o FALSO si no lo es. int es_bisiesto( unsigned anyo ); Cualquier variable global que utilice el módulo y que no haya sido especificada en los párrafos

anteriores, debe ser declarada como privada al módulo.