Professional Documents
Culture Documents
2 de abril de 2002
Indice general
1. Descripci on del ordenador 1.1. Introducci on . . . . . . . . . . . . . . . . . . . . . 1.2. Arquitectura de un ordenador de prop osito general 1.2.1. La unidad central de proceso . . . . . . . . 1.2.2. Memoria . . . . . . . . . . . . . . . . . . 1.2.3. Unidad de entrada/salida . . . . . . . . . . 1.2.4. Buses . . . . . . . . . . . . . . . . . . . . 1.3. Codicaci on de la informaci on . . . . . . . . . . . 1.3.1. Sistemas de numeraci on posicionales . . . 1.3.2. Codicaci on de n umeros en un ordenador . 1.3.3. Codicaci on de caracteres alfanum ericos . 1.4. Codicaci on del programa . . . . . . . . . . . . . 1.4.1. Compiladores e int erpretes . . . . . . . . . 2. El primer programa en C 2.1. Introducci on . . . . . . . . . . . . . . . . . . . . 2.2. El sistema operativo . . . . . . . . . . . . . . . . 2.3. Creaci on de un programa en C . . . . . . . . . . 2.3.1. Primer paso: Edici on del programa fuente 2.3.2. Segundo paso: Compilaci on . . . . . . . 2.3.3. Tercer paso: Enlazado . . . . . . . . . . 2.4. Nuestro primer programa en C . . . . . . . . . . 2.4.1. Comentarios . . . . . . . . . . . . . . . 2.4.2. Directivas del preprocesador . . . . . . . 2.4.3. La funci on principal . . . . . . . . . . . 2.4.4. Las funciones . . . . . . . . . . . . . . . 2.4.5. Finalizaci on del programa . . . . . . . . 2.4.6. La importancia de la estructura . . . . . . 3. Tipos de datos 3.1. Introducci on . . . . . . . . . 3.2. Variables . . . . . . . . . . 3.3. Tipos b asicos de variables . 3.3.1. N umeros enteros . . 3.3.2. N umeros reales . . . 3.3.3. Variables de car acter 3.4. M as tipos de variables . . . . 3.5. Constantes . . . . . . . . . . 7 7 7 8 8 8 8 9 9 11 12 14 14 17 17 17 17 18 18 19 19 20 20 21 22 22 22 25 25 25 26 26 26 27 27 27
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . . 3
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
4 3.6. Tama no de las variables . . . . . . . . . . . . 3.7. Escritura de variables en pantalla . . . . . . . 3.7.1. Escritura de variables de tipo entero . 3.7.2. Escritura de variables de tipo real . . 3.7.3. Escritura de variables de tipo car acter 3.7.4. Escritura de variables con formato . . 3.8. Lectura de variables por teclado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
INDICE GENERAL
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 29 30 31 32 32 33 35 35 35 35 36 37 37 37 38 38 39 40 43 43 44 45 46 49 49 50 50 53 53 55 56 56 57 59 59 59 61 61 62 63 63 66
4. Operadores en C 4.1. Introducci on . . . . . . . . . . . . . . . . . . . . 4.2. El operador de asignaci on . . . . . . . . . . . . . 4.3. Operadores para n umeros reales . . . . . . . . . 4.4. Operadores para n umeros enteros . . . . . . . . . 4.4.1. Operador resto . . . . . . . . . . . . . . 4.4.2. Operadores de incremento y decremento . 4.5. Operador unario - . . . . . . . . . . . . . . . . . 4.6. De vuelta con el operador de asignaci on . . . . . 4.7. Conversiones de tipos . . . . . . . . . . . . . . . 4.7.1. El operador asignaci on y las conversiones 4.8. Ejemplos . . . . . . . . . . . . . . . . . . . . . 5. Control de ujo: for 5.1. Introducci on . . . . . . . . . . 5.2. Sintaxis del bucle for . . . . . 5.2.1. Operadores relaciones 5.3. Doble bucle for . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . . . . . . . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
6. Control de ujo: while, do-while 6.1. Introducci on . . . . . . . . . . . . . . 6.2. Sintaxis del bucle while . . . . . . . 6.2.1. Ejemplos . . . . . . . . . . . 6.3. Sintaxis del bucle do-while. . . . . . 6.3.1. Ejemplos . . . . . . . . . . . 6.4. Es el bucle for igual que el while? . 6.4.1. Entonces cu al elijo? . . . . . 6.5. El ndice del bucle for sirve para algo 6.6. Ejercicios . . . . . . . . . . . . . . . 7. Control de ujo: if 7.1. Introducci on . . . . . . . . . . . 7.2. Sintaxis del bloque if . . . . . . 7.3. Formato de las condiciones . . . 7.3.1. Operadores relacionales 7.3.2. Operadores l ogicos . . . 7.4. Valores de verdadero y falso . . 7.5. Bloque if else-if . . . . . . 7.6. Ejercicios . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
INDICE GENERAL
8. Control de ujo: switch-case, break y continue 8.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . 8.2. La construcci on switch-case . . . . . . . . . . . . 8.2.1. Introducci on . . . . . . . . . . . . . . . . . 8.2.2. Sintaxis de la construcci on switch-case . . 8.2.3. Ejemplos . . . . . . . . . . . . . . . . . . . 8.3. La sentencia break . . . . . . . . . . . . . . . . . . 8.3.1. La sentencia break y el switch-case . . . . 8.3.2. La sentencia break y los bucles . . . . . . . 8.3.3. Ejemplos . . . . . . . . . . . . . . . . . . . 8.4. La sentencia continue . . . . . . . . . . . . . . . . 8.4.1. Ejemplos . . . . . . . . . . . . . . . . . . . 8.5. Ejercicios . . . . . . . . . . . . . . . . . . . . . . .
5 67 67 67 67 68 69 73 73 74 74 76 77 79 81 81 81 83 84 86 92 92 95 97 97 98 99 100 102 104 105 105 105 106 108 108 111 111 113 113 113 114 115 116 117 119 120 122
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
9. Vectores y Matrices 9.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.2. Vectores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.3. Cadenas de caracteres . . . . . . . . . . . . . . . . . . . . . . 9.3.1. Cadenas de caracteres de longitud variable . . . . . . 9.3.2. Funciones est andar para manejar cadenas de caracteres 9.4. Matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.5. Utilizaci on de #define con vectores y matrices . . . . . . . . 9.6. Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10. Funciones 10.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . 10.2. Estructura de una funci on . . . . . . . . . . . . . . . 10.3. Prototipo de una funci on . . . . . . . . . . . . . . . 10.4. Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . 10.5. Paso de argumentos a las funciones. Variables locales 10.6. Salida de una funci on y retorno de valores . . . . . . 10.7. Funciones sin argumentos . . . . . . . . . . . . . . . 10.8. Ejemplos . . . . . . . . . . . . . . . . . . . . . . . 10.8.1. Elevar al cuadrado . . . . . . . . . . . . . . 10.8.2. Factorial . . . . . . . . . . . . . . . . . . . 10.9. Paso de vectores, cadenas y matrices a funciones . . 10.9.1. Paso de vectores . . . . . . . . . . . . . . . 10.9.2. Paso de cadenas a funciones . . . . . . . . . 10.9.3. Paso de matrices a funciones . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
. . . . . . . . . . . . . .
11. Punteros 11.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . 11.2. Declaraci on e inicializaci on de punteros . . . . . . . . . . . 11.2.1. El operador unario & . . . . . . . . . . . . . . . . . 11.2.2. El operador unario * . . . . . . . . . . . . . . . . . 11.3. Operaciones con punteros . . . . . . . . . . . . . . . . . . . 11.4. Punteros y vectores . . . . . . . . . . . . . . . . . . . . . . 11.4.1. Equivalencia de punteros y vectores . . . . . . . . . 11.5. Punteros y funciones . . . . . . . . . . . . . . . . . . . . . 11.5.1. Retorno de m as de un valor por parte de una funci on
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
6 11.6. Asignaci on din amica de memoria . . . . . . 11.6.1. Las funciones calloc() y malloc() 11.6.2. La funci on free() . . . . . . . . . . 11.6.3. Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
INDICE GENERAL
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122 123 124 125 129 129 129 131 132 132 132 134 138 141 141 141 142 143 144 145 146 147 148 149 152 152 153 154 156
12. Archivos 12.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.2. Apertura de archivos. La funci on fopen() . . . . . . . . . . . . . . . . 12.3. Cierre de archivos. La funci on fclose(). . . . . . . . . . . . . . . . . 12.4. Lectura y escritura en archivos. Las funciones fprintf() y fscanf(). 12.4.1. La funci on fprintf() . . . . . . . . . . . . . . . . . . . . . . 12.4.2. La funci on fscanf() . . . . . . . . . . . . . . . . . . . . . . . 12.5. Ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12.6. Funciones de entrada y salida a archivo sin formato . . . . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . .
13. Estructuras de datos 13.1. Introducci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.2. Declaraci on y denici on de estructuras . . . . . . . . . . . . . . . . . . . . 13.3. La sentencia typedef . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.4. Acceso a los miembros de una estructura . . . . . . . . . . . . . . . . . . . 13.5. Ejemplo: Suma de n umeros complejos . . . . . . . . . . . . . . . . . . . . 13.6. Estructuras y funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.7. Punteros a estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.7.1. Paso de punteros a estructuras a funciones . . . . . . . . . . . . . . 13.8. Vectores de estructuras . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.8.1. Ejemplo: C alculo del producto escalar de dos vectores de complejos 13.9. Estructuras y archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13.9.1. Diferencia entre archivos de texto y archivos binarios . . . . . . . . 13.9.2. Para almacenar estructuras en archivos de texto . . . . . . . . . . . 13.9.3. Para almacenar estructuras en archivos binarios . . . . . . . . . . . 13.9.4. Funciones fread, fwrite y fseek . . . . . . . . . . . . . . . . .
Cap tulo 1
Datos de Entrada
Ordenador
a
Datos de Salida
Datos de Entrada
Ordenador
b
Datos de Salida
C.P.U.
Memoria
E/S
Pantalla
Bus de control
1.2.2. Memoria
Es la unidad encargada de almacenar tanto el programa que le dice a la CPU lo que tiene que hacer, como los datos con los que tiene que trabajar. Desde el punto de vista del programador consiste en un vector de posiciones de memoria a las cuales se puede acceder (leer o n. escribir) sin m as que especicar su direccio
1.2.4. Buses
La interconexi on entre los elementos del ordenador se realiza mediante un bus de datos, gracias al cual el procesador lee o escribe datos en el resto de dispositivos 2 , un bus de direcciones por el cual el procesador indica a los dispositivos qu e posici on quiere leer o escribir 3 , un bus de control mediante el cual el procesador les indica el momento en el que va a realizar el acceso, si e ste va a ser de lectura o escritura, etc. Este bus de control tambi en permite a los
2 Se dice entonces que este bus es bidireccional, pues permite que la informaci on viaje desde el procesador a los dispositivos o viceversa. 3 Este bus en cambio es unidireccional, pues la direcci on viaja siempre del procesador a los dispositivos.
dispositivos pedir la atenci on del procesador ante un suceso mediante un mecanismo llamado interrupci on.
1000
100
10
103
102
101
100 (1.1)
Como se puede apreciar, los pesos por los que se multiplica cada d gito se forman elevando un n umero, al que se denomina base a la potencia indicada por la posici on del d gito. Al d gito que est a m as a la derecha se le denomina d gito menos signicativo y se le asigna la posici on s signicativo. 0; al que est a m as a la izquierda se le denomina d gito ma Como en un sistema digital s olo existen dos d gitos binarios, 0 y 1, los n umeros dentro de un ordenador han de representarse en base 2 5 , as por ejemplo6 : 110112
24
23
22
21
20
16
27
(1.2)
Esta base es muy adecuada para las m aquinas, pero bastante desagradable para los humanos, por ejemplo el n umero 1327 en binario es 10100101111 2; bastante m as dif cil de recordar, escribir e interpretar. Por ello los humanos, que para eso somos m as listos que las m aquinas, usamos sistemas de numeraci on m as aptos a nuestras facultades. La base 10 es la
palabra bit viene del ingl es binary digit. representar n umeros en una base n hacen falta n s mbolos distintos. 6 Para indicar n umeros representados en una base distinta a la base 10, se pondr a en estos apuntes la base como sub ndice.
5 Para 4 La
10
m as c omoda para operar con ella debido a que las potencias de la base son f aciles de calcular y a que la suma expresada en (1.1) se calcula sin ninguna dicultad. Por el contrario en base 2 ni las potencias de la base ni la suma nal (1.2) son tan f aciles de calcular. Uno de los inconvenientes de la representaci on de n umeros en binario es su excesiva longitud. Para simplicar las cosas se pueden convertir los n umeros binarios a decimales y trabajar con ellos, pero eso no es tarea f acil, pues hay que realizar una operaci on como la indicada en (1.2). Para solucionar este problema, se pueden usar bases que son tambi en potencias de 2, de forma que la notaci on sea compacta y f acil de manejar por los humanos y que adem as las conversiones a binario sean f aciles de realizar. Las dos bases m as usadas son la octal (base 8) y la hexadecimal (base 16). En la tabla 1.1 se muestran las equivalencias entre binario, decimal, octal y hexadecimal. Cuadro 1.1: Conversi on entre bases Binario 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 10000 10001 Decimal 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Octal 0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 20 21 Hexadecimal 0 1 2 3 4 5 6 7 8 9 A B C D E F 10 11
Como se puede apreciar, los n umeros octales s olo usan 8 s mbolos (del 0 al 7) y la base hexadecimal precisa 16 (del 0 al 9 y se a naden las letras de la A a F). La conversi on entre binario y octal se realiza agrupando los bits en grupos de tres y realizando la transformaci on de cada grupo por separado, seg un la tabla 1.1: 101001011112
24578
y entre binario y hexadecimal se hace igual, pero agrupando los bits de cuatro en cuatro: 101001011112
52F16
La notaci on octal se us o en los ordenadores primitivos, en los que el ordenador comunicaba los resultados con lamparitas (como salen en las pel culas) que se agrupaban en grupos de 3 para su interpretaci on en base octal. Sin embargo, con el avance de la t ecnica, se vio que era conveniente usar n umeros binarios de 8 bits, a los que se les denomin o bytes. Estos bytes se representan mucho mejor como dos d gitos hexadecimales, uno para cada grupo de 4 bits7 . El caso es que hoy en d a la notaci on octal est a francamente en desuso, siendo lo m as
7 adem as
a cada grupo de 4 bits, representable directamente por un d gito hexadecimal, se le denomina nible.
11
corriente expresar un n umero binario mediante su equivalente hexadecimal cuando ha de ser interpretado por un ser humano (dentro del ordenador por supuesto todo esta en binario).
Codicaci on de numeros enteros Si se desea codicar un n umero natural, la manera m as directa es usar su representaci on en binario, tal como se ha visto en la Secci on 1.3.1. No obstante, los procesadores trabajan con magnitudes de un n umero de bits predeterminados, t picamente m ultiplos pares de un byte. As por ejemplo los procesadores tipo Pentium pueden trabajar con n umeros de 1, 2 y 4 bytes, o lo que es lo mismo, de 8, 16 y 32 bits. Por tanto, antes de introducir un n umero natural en el ordenador, hemos de averiguar el n umero de bits que hacen falta para codicarlo, y luego decirle al ordenador que use un tipo de almacenamiento adecuado. Para averiguar cuantos bits hacen falta para almacenar un n umero natural no hace falta convertirlo a binario. Si nos jamos en que con n bits se pueden representar n umeros naturales que van desde 0 a 2n 1, s olo hace falta comparar el n umero a codicar con el rango del tipo en el que lo queremos codicar. As por ejemplo si queremos almacenar el n umero 327 en 8 bits, como el rango de este tipo es de 0 a 28 1 255, vemos que en 8 bits no cabe, por tanto hemos de almacenarlo en 16 bits, que al tener un rango de 0 a 2 16 1 65535 nos vale perfectamente. Para la codicaci on de n umeros negativos existen varios m etodos, aunque el m as usado es la codicaci on en complemento a dos, que posee la ventaja de que la operaci on suma es la misma que para los n umeros positivos. En esta codicaci on el bit m as signicativo vale cero para los n umeros positivos y uno para los negativos. En el resto de bits est a codicada la magnitud, aunque el m etodo de codicaci on se escapa de los nes de esta introducci on. Lo que si ha de quedar claro es que como ahora tenemos en n bits n umeros positivos y negativos, el rango total de 0 a 2n 1 se divide ahora en 2n 1 a 2n 1 1. As pues si por ejemplo queremos codicar el n umero -227 necesitaremos 16 bits, pues el rango de un n umero codicado en complemento a dos de 8 bits es de 27 a 27 1, es decir, de 128 a 127. En cambio el rango disponible en 16 bits es de 32768 a 32767.
Para la mayor a de las aplicaciones de c alculo cient co se necesita trabajar con n umeros reales. Estos n umeros reales se representan seg un una mantisa de parte entera 0 y un exponente (por ejemplo 3.701 se almacena como 0 3701 10 1). Nuevamente tanto la mantisa como el exponente se almacenan en binario, seg un una codicaci on que escapa de los nes de esta breve introducci on. Antiguamente cada ordenador usaba un tipo de codicaci on en punto otante distinta. Por ello surgi o la necesidad de buscar un est andar de almacenamiento com un para todas las plataformas. De ah surgi o el est andar IEEE 854 8 que es el usado actualmente por la mayor a de los procesadores para la realizaci on de las operaciones en coma otante. Este est andar dene tanto la codicaci on de los n umeros como la manera de realizar las operaciones aritm eticas con ellos (incluidos los redondeos).
Oct Dec Hex Char Oct Dec Hex Char -----------------------------------------------------------000 0 00 NUL \0 100 64 40 @ 001 1 01 SOH 101 65 41 A 002 2 02 STX 102 66 42 B 003 3 03 ETX 103 67 43 C 004 4 04 EOT 104 68 44 D
8 IEEE son las siglas del Institute of Electrical and Electronics Engineers, que es una asociaci on americana de ingenieros que se dedica entre otras tareas a la denici on de est andares relacionados con la ingenier a el ectrica y electr onica. 9 ISO son las siglas de International Standars Organization.
13
\\
14 067 070 071 072 073 074 075 076 077 55 56 57 58 59 60 61 62 63 37 38 39 3A 3B 3C 3D 3E 3F 7 8 9 : ; < = > ?
Por ejemplo el car acter a se codica como el n umero 97 (01100001 en binario) y el car acter A se codica como 65 (00100001 en binario). Tambi en cabe destacar que el orden num erico dentro de la tabla coincide con el orden alfab etico de las letras, lo cual es muy u til cuando se desean ordenar caracteres por orden alfab etico.
15
Como ejemplos de lenguajes que son normalmente interpretados tenemos el BASIC y el Java. El principal inconveniente de los lenguajes interpretados es que el proceso de traducci on lleva tiempo, y hay que realizarlo una y otra vez. Si se desea eliminar el tiempo de traducci on del programa de alto nivel, podemos traducirlo todo de una vez y generar un programa en c odigo m aquina que ser a ejecutable directamente por el procesador. Con ello la ejecuci on del programa ser a mucho m as r apida, pero tiene como inconveniente que cada vez que se cambia algo en el programa hay que volver a traducirlo a c odigo m aquina. Al proceso de traducci on del programa a c odigo m aquina se le denomina compilaci on, y al programa encargado de ello compilador. Ejemplos de lenguajes que son normalmente compilados son el C, el Pascal o el FORTRAN.
16
Cap tulo 2
El primer programa en C
2.1. Introducci on
Una vez descrito el funcionamiento b asico de un ordenador, vamos a realizar nuestro primer programa en lenguaje C. Veremos en este cap tulo las herramientas necesarias para crear programas, almacenarlos, compilarlos y ejecutarlos.
17
18
19
denomina c odigo objeto (con extensi on .obj o .o). Si existe alg un error sint actico en el c odigo fuente el compilador generar a un mensaje de error indicando la l nea en la que encontr o el problema y dici endonos la causa del error. En la mayor a de los casos el error estar a en la l nea indicada, aunque puede estar en l neas anteriores. Si el programa compila sin errores, podemos pasar a la siguiente fase. Si no, habr a que volver a editar el programa fuente para corregir los errores y repetir el proceso hasta que el compilador termine su tarea con e xito.
20
2.4.1. Comentarios
En primer lugar vemos las l neas: /* Programa: Hola * * Descripci on: Escribe un mensaje en la pantalla del ordenador * * Revisi on 0.0: 16/02/1998 * * Autor: Jos e Daniel Mu noz Fr as */ que forman la cha del programa. La nalidad de esta cha es la documentaci on del programa, de forma que cualquier persona sepa el nombre, la nalidad, la revisi on y el autor del programa sin m as que leer el principio del archivo del c odigo fuente. Si observamos m as detenidamente las l neas anteriores veremos que comienzan por /* y terminan por */ (los dem as * se han colocado con nes decorativos). En C todo el texto encerrado entre /* y */ se denomina comentario y es ignorado por el compilador, de forma que el programador pueda escribir lo que quiera con nes de documentaci on del c odigo. Esto, que puede parecer una tonter a en un programa tan simple como el mostrado en esta secci on, es fundamental cuando se abordan programas m as grandes. Existen dos tipos de comentarios2: los que acabamos de ver, tipo cha del programa, y los que se insertan en el c odigo para aclarar operaciones que no sean obvias. Ejemplos de este tipo de comentarios son: andar */ #include <stdio.h> /* Declara las funciones de entrada-salida est printf("Hola!\n"); /* Imprimo el mensaje */ En este caso, al ser el primer programa que realizamos, los comentarios incluidos son obvios, habi endose a nadido simplemente con nes ilustrativos.
el punto de vista del programador, pues para el compilador todos son iguales.
21
La utilidad de incluir cheros es la de poder escribir en un chero declaraciones de funciones, de estructuras de datos. . . que sean usadas repetidamente por nuestros programas, de forma que no tengamos que escribir cada vez que realizamos un nuevo programa dichas declaraciones. En este ejemplo, antes de poder usar la funci on printf(), es necesario decirle al compilador que esa funci on existe en una biblioteca, y que no debe preocuparse si no est a en nuestro archivo de c odigo fuente, pues ya se encargar a el enlazador de a nadirla al programa ejecutable. Existen m as directivas del preprocesador que se ir an estudiando a lo largo del curso.
22
23
Este programa desde el punto de vista del compilador es id entico al programa original, es decir, el compilador generar a el mismo programa ejecutable que antes. El inconveniente de este estilo de programaci on es que el c odigo fuente es bastante m as dif cil de leer y entender. Por tanto, aunque al compilador le da lo mismo la estructura que posea nuestro c odigo fuente, a los pobres lectores de nuestra obra s que les interesa. Por tanto a lo largo del curso, y especialmente en el laboratorio, se penalizar a enormemente a aquellos alumnos rebeldes que no sigan las normas de estilo de un buen programador en C, que se resumen en: Escribir al principio de cada programa un comentario que incluya el nombre del programa, describa la tarea que realiza, indique la revisi on, fecha y el nombre del programador. Cada instrucci on ha de estar en una l nea separada. Las instrucciones pertenecientes a un bloque han de estar sangradas respecto a las llaves que lo delimitan: { printf("Hola!\n"); } Hay que comentar todos los aspectos importantes del c odigo escrito.
24
Cap tulo 3
Tipos de datos
3.1. Introducci on
Este cap tulo introduce las variables y los tipos de datos b asicos de C (int, float, double y char). Tambi en se explican las funciones b asicas de entrada y salida que van a permitir leer desde teclado y escribir por pantalla variables de cualquier tipo. Tanto las variables como las funciones de entrada y salida se utilizan en todos los programas.
3.2. Variables
Todos los datos que maneja un programa se almacenan en variables. Estas variables tienen un nombre arbitrario denido por el programador e invisible para el usuario del programa. Ni el resultado, ni la velocidad de c alculo ni la cantidad de memoria utilizada dependen del nombre de las variables. Algunos ejemplos son x, i, variable temporal, Resistencia2. . . Sin embargo hay que tener en cuenta que existen algunas restricciones en el nombre de las variables: Los nombres pueden contener letras a..z, A..Z, d gitos 0..9 y el car acter ; sin embargo siempre deben comenzar por una letra. Otros caracteres de la tabla ASCII como por ejemplo el gui on -, el punto ., el espacio , el d olar $ . . . no son v alidos. Algunos nombres de variables no son v alidos por ser palabras reservadas del lenguaje, por ejemplo if, else, for, while, etc. El compilador dar a un error si se intentan utilizar nombres de variables no v alidos. Es importante recordar que C es un lenguaje de programaci on que distingue min us-culas y may usculas y por lo tanto la variable X1 es diferente a la variable x1. Generalmente en C todos los nombres de variables se denen en min usculas y en los nombres formados por varias palabras se utiliza el car acter para separarlas. Por ejemplo suele escribirse variable temporal en lugar de variabletemporal, que resulta m as confuso de leer para el propio programador y para otros programadores que revisen su c odigo. 25
26
27
En general los modicadores short y long cambian el tama no de la variable b asica y por lo tanto cambian el rango de valores que pueden almacenar. Por otro lado el modicador unsigned, que no puede aplicarse a los tipos reales, modica el rango de las variables sin cambiar el espacio que ocupan en memoria. Por ejemplo, en un ordenador donde el tipo int puede almacenar valores en el rango desde -32768 hasta 32767, una variable unsigned int tendr a un rango desde 0 hasta 65535, ya que no utiliza n umeros negativos. En ambos casos el n umero de valores diferentes que pueden tomar las variables es el mismo (65536 valores posibles = 216 ).
3.5. Constantes
Las constantes son, en general, cualquier n umero que se escribe en el c odigo y que por lo tanto no puede modicarse una vez que el programa ha sido compilado y enlazado. Si
28
escribimos por ejemplo i=7, la variable i tomar a siempre el valor 7, ya que este n umero es jo en el programa: es una constante. El compilador considera como constantes de tipo int todos los numeros enteros, positivos o negativos, que se escriben en un programa. Para que estas constantes se consideren de tipo long deben llevar al nal una L o una l, para que se consideren de tipo unsigned deben llevar una u o una U y los enteros largos sin signo debe llevar ul o bien UL. Ver el siguiente ejemplo: int entero; unsigned sin_signo; long largo; unsigned long sin_signo_largo; entero=7; sin_signo=55789u; largo=-300123456L; /*es mejor L, porque l se confunde con un 1 */ sin_signo_largo=2000000000UL; Las constantes de tipo entero tambi en pueden escribirse en octal y en hexadecimal, lo que nos ahorra tener que hacer las conversiones a decimal mientras escribimos el programa. Los n umeros en base 8 se escriben empezando con cero y en base 16 empezando por 0x o por 0X. Las tres l neas siguientes son equivalentes: i=31; /*Decimal*/ i=037; /*Octal*/ i=0x1F; /*Hexadecimal, tambi en vale poner 0x1f*/ Los numeros reales se identican porque llevan un punto (123.4) o un exponente (1e-2) o ambas cosas. En estos casos la constante se considera de tipo double. Las constantes de caracteres se escriben entre comillas simples como en el siguiente ejemplo: char c; c=z; En realidad las constantes de caracteres se convierten autom aticamente a tipo int de acuerdo a la tabla ASCII, o sea que la instrucci on anterior es equivalente a escribir: char c; c=122; /* Esta asignaci on es correcta en C, aunque c es de tipo char, no int */
Determinados caracteres especiales se pueden escribir utilizando secuencias de escape que comienzan por el car acter \, como \n en el programa del cap tulo 2. Los ejemplos m as t picos son los siguientes: \n \r \0 \t \f cambio de l nea (newline) retorno de carro (return) car acter nulo (NULL). No confundir con el car acter 0. TAB cambio de p agina (form feed)
29
En el programa anterior la funci on printf sustituye el texto %d por el valor de la variable dia expresada como un entero. Este programa escribe por pantalla la siguiente l nea: Hoy es el d a 47 del a no Cada tipo de variable requiere utilizar una letra diferente despu es del car acter %, los formatos m as normales para los tipos b asicos de variables se muestran en la siguiente tabla: formato %d %f %c tipo de variable int float y double char
La funci on printf puede escribir varias variables a la vez, del mismo tipo o de tipo distinto, pero hay que tener cuidado para incluir en la cadena de caracteres tantos % como n umero de variables. Adem as hay que tener cuidado para introducir correctamente los formatos de cada tipo de variable. Ejemplo: /********************* Programa: Tipos.c Descripci on: Escribe variables de distintos tipos Revisi on 0.1: 16/FEB/1999 Autor: Rafael Palacios *********************/ #include <stdio.h> main(void) { int dia; double temp; char unidad; dia=47; temp=11.4; unidad=C; printf("D a %d, Temperatura %f %c\n",dia,temp,unidad); } --------------- Salida -------------------------D a 47, Temperatura 11.400000 C
31
Los formatos para variables de tipo entero son %d para mostrar el valor en decimal, %o para mostrarlo en octal y %X o %x para mostrarlo en hexadecimal3. En formato octal y hexadecimal se muestra el valor sin signo. printf("Decimal %d, en octal %o, en hexadecimal %X\n",dia,dia,dia); escribe la siguiente l nea en pantalla: Decimal 47, en octal 57, en hexadecimal 2F Los tipos de variables derivados de int, como short, long, unsigned. . . utilizan los caracteres de formato que se muestran en la siguiente tabla: formato %d %hd %ld %u %hu %lu tipo de variable int short long unsigned int unsigned short unsigned long
32
El formato %g es el m as seguro cuando no se conoce de qu e orden es el valor a escribir ya que elige autom aticamente entre %f (que es m as bonito) y %e (donde cabe cualquier n umero por grande que sea).
--> -->
: 1024.251000: : 1024.2510:
33
:1024.2510 : Alineaci on izquierda : 1024.3: Redondea correctamente :1024.251000: No cabe-> por defecto :1024.251: Tama no por defecto con 3 dec. :1.024251e+03: : 1.0243e+03: : 1.0e+03: :1.024251e+03: :1.024e+03: 12 caracteres Idem. pero con 4 dec. Idem. pero con 1 dec. No cabe. Por defecto 3 decimales.
34
Ejercicios
1. Escribe una tabla con todos los tipos de datos que incluya los formatos que utiliza cada uno con printf y scanf
Cap tulo 4
Operadores en C
4.1. Introducci on
En el cap tulo anterior se han visto los tipos de datos b asicos del lenguaje C, c omo leer estos datos desde teclado y c omo imprimirlos en la pantalla. Puesto que los ordenadores son equipos cuya misi on es el proceso de datos, los lenguajes de programaci on proveen al programador con una serie de operadores que permiten realizar las operaciones necesarias sobre los datos. En este cap tulo vamos a estudiar los distintos operadores soportados por el lenguaje C.
35
36
CAPITULO 4. OPERADORES EN C
Las operaciones de suma, resta, multiplicaci on y divisi on se representan en C mediante los operadores +, -, * y / respectivamente. Supongamos que estamos desarrollando un programa de facturaci on y que tenemos que calcular el importe total a partir de la base imponible para un IVA del 16 %. La forma de realizarlo ser a: total = base * 1.16; En donde se realiza el producto de la variable base por la constante 1.16 para despu es asignar el resultado a la variable total. En una misma expresi on se pueden combinar varios operadores, aplic andose en este caso las mismas reglas de precedencia usadas en matem aticas, es decir, en primer lugar se realizan las multiplicaciones y las divisiones, seguidas de las sumas y restas. Cuando las operaciones tienen la misma precedencia, por ejemplo 2*pi*r, e stas se realizan de izquierda a derecha. Siguiendo con el ejemplo anterior, si tenemos dos art culos de los cuales el primero es un libro, al que se le aplica un 4 % de IVA y el segundo una tableta de chocolate, a la que se le aplica (injustamente) un IVA del 16 %, el c alculo del importe total de la factura se realizar a en C mediante la siguiente l nea de c odigo: total = 1.04*ValorLibro + 1.16*ValorChocolate; En donde en primer lugar se eval ua 1.04*ValorLibro, seguidamente 1.16*ValorChocolate y por u ltimo se suman los resultados de ambas operaciones, asign andose el valor de la suma a la variable total. Si se desea alterar el orden de precedencia preestablecido, se pueden usar par entesis al igual que en matem aticas, eso si, s olo par entesis; las llaves y los corchetes tienen otros signicados en el lenguaje. Siguiendo con el ejemplo anterior, si los dos art culos tienen el mismo tipo de IVA, el c alculo del total de la factura se escribir a en C: total = 1.16*(ValorChocolate + ValorHelado); En este caso se realiza en primer lugar la suma de ValorChocolate a ValorHelado y el resultado se multiplicar a por la constante 1.16, asign andose el resultado a la variable total. Si hubi esemos escrito la l nea anterior sin los par entesis, es decir: total = 1.16 * ValorChocolate + ValorHelado; /*Factura mal calculada*/ El programa multiplicar a 1.16 por ValorChocolate y al resultado le sumar a el ValorHelado, por lo que ser amos perseguidos duramente por el sco por facturar helados sin IVA.
37
Como los dos operadores tienen la misma precedencia, la expresi on se eval ua de izquierda a derecha, con lo cual en primer lugar se efect ua 1/2 con el resultado antes anunciado de 0 patatero, que al multiplicarlo despu es por el 3 vuelve a dar 0, como todo el mundo habr a ya adivinado. Si en cambio se escribe: resultado = 3*1/2; El resultado ahora es de 1, con lo que se aprecia que en lenguaje C cuando se trabaja con n umeros enteros lo de la propiedad conmutativa no funciona nada bien, y deja bien claro que, en contra de la creencia popular, los ordenadores no son infalibles realizando c alculos matem aticos, sobre todo cuando el programador no sabe muy bien lo que est a haciendo y descuida temas como los redondeos de las variables o los rangos m aximos de los tipos.
4.5. Operador unario Hasta ahora, todos los operadores que se han discutido eran binarios, es decir, operaban con dos n umeros: el situado a su izquierda con el situado a su derecha. El operador unario toma s olo un valor situado a su derecha y le cambia el signo. As en: i = -c; el operador - toma el valor de la variable c y le cambia el signo. Por tanto si c vale 17, al nalizar la ejecuci on de la instrucci on, la variable i contendr a el valor -17.
3 Adem as todos los procesadores tienen instrucciones especiales de incremento y decremento, usualmente m as r apidas que la de suma normal, con lo que estos operadores le permiten al compilador usar ese tipo de instrucciones, consiguiendo un programa m as eciente.
38
CAPITULO 4. OPERADORES EN C
39
La u ltima l nea quiere decir que aunque los operandos de una operaci on sean los dos de aticamente a int, para realizar la tipo char o short, el compilador los promociona autom operaci on con mayor precisi on. Cuidado con la divisi on y los enteros En una expresi on como la siguiente: resultado = 1/2*d; como todos los operadores tiene la misma precedencia, la expresi on se eval ua de izquierda a derecha, realiz andose en primer lugar la operaci on 1/2, en la que como los dos operandos son dos constantes de tipo int, se realiza la divisi on entera, obteni endose un 0 como resultado. La a el siguiente operaci on a realizar ser a la multiplicaci on que, si d es de tipo double, convertir resultado anterior (0) a double y luego lo multiplicar a por d, obteni endose un resultado nada deseado. Si se realiza la operaci on al rev es, es decir, d*1/2 el resultado ser a ahora correcto por qu e? Para evitar este tipo de situaciones conviene evitar usar constantes enteras cuando trabajemos en expresiones con valores reales. As la mejor manera de evitar el problema anterior es escribir: resultado = 1.0/2.0*d Ahora bien, este m etodo no es v alido cuando tenemos que usar en una expresi on variables enteras junto con variables reales. En este caso puede ser necesario forzar una conversi on que el compilador no realizar a de modo autom atico. Por ejemplo, si las variables i1 e i2 son de tipo int y la variable d es de tipo double, en la expresi on i1/i2*d se realizar a la divisi on de n umeros enteros, con el consabido peligro y falta de precisi on. Para conseguir que la divisi on se realice con n umeros de tipo double podemos reordenar las operaciones como se hizo antes, o mejor, forzar al compilador a que realice la conversi on. Para forzar la conversi on se usa el operador unario molde, llamado cast en ingl es, que tiene el formato: (tipo al que se desea convertir) variable a convertir El ejemplo anterior forzando las conversiones quedar a como: resultado = (double) i1 / (double) i2 * d; Como el alumno aventajado habr a notado s olo ser a necesario poner un molde en cualquiera de las dos variables enteras Por qu e?
40
CAPITULO 4. OPERADORES EN C
parte decimal), y en algunos casos resultados err oneos (por ejemplo al asignar un valor grande de tipo long a un int). Como resumen a esta secci on cabe decir que se ha de ser extremadamente cuidadoso/a cuando en una instrucci on se mezclen variables y constantes de tipos distintos si se desean evitar resultados err oneos, que a menudo s olo se dan en ciertas situaciones, funcionando el programa correctamente en las dem as.
4.8. Ejemplos
Para aanzar los conceptos vamos a estudiar a continuaci on algunos ejemplos de expresiones matem aticas en C. main(void) { int ia, ib; int ires; double da, db; double dres; ia ib da db = = = = 1; 3; 2.3; 3.7; = = = = = ia + (ib*2 + ia*3); /* Ej 1 */ ia + (ib*2 + ia*3); /* Ej 2 */ ia + da*(ib/2.0 + ia/2.0); /* Ej 3*/ ia + da*(ib/2.0 + ia/2.0); /* Ej 4*/ -da*(ia + ib*(da + db/3.0)); /* Ej 5*/
En el primer ejemplo, se eval ua en primer lugar la expresi on que est a entre par entesis. Como esta expresi on contiene multiplicaciones y sumas, en primer lugar se realizan las multiplicaciones, sum andose seguidamente los resultados. Por u ltimo al resultado se le suma ia y se almacena el n umero obtenido en la variable ires. Como todas las variables involucradas son de tipo int no se realiza ninguna conversi on de tipos. N otese que en este caso los par entesis son innecesarios. El segundo ejemplo es id entico al primero, salvo que el resultado, que recordemos es de tipo int, se almacena en una variable de tipo double. Se realizar a por tanto una conversi on de tipos del resultado de int a double antes de realizar la asignaci on, lo que no debe de dar ning un problema. En el tercer ejemplo se realizan en primer lugar las divisiones de dentro del par entesis, en las que debido a que el segundo operando es una constante real, los operandos enteros se endose por tanto un resultado de tipo double que se multiplica convierten a double, obteni por da para sumarlo despu es a ia, que al ser un int ha de convertirse previamente a double. El resultado se almacena en una variable de tipo double, por lo que no se realizan m as conversiones de tipos. En el cuarto ejemplo el proceso es el descrito para el ejemplo anterior, salvo que al asignar el n umero de tipo double que resulta a uno de tipo int, se realiza una conversi on de double
4.8. EJEMPLOS
41
a int, por lo que se almacena en ires la parte entera del resultado de la expresi on, siempre y cuanto esta est e dentro el rango del tipo int, perdi endose para siempre la parte decimal. Por u ltimo en el ejemplo quinto se muestra la posibilidad de anidar varias expresiones entre par entesis. En este caso se eval ua en primer lugar el par entesis interno, el resultado se multiplica por ib y despu es se suma ia al resultado anterior. Todo ello se multiplica por da cambiado de signo. Que conversiones de tipos se han realizado en este ejemplo?
Ejercicios
1. Calcular el valor que resulta de cada una de las expresiones del programa anterior.
42
CAPITULO 4. OPERADORES EN C
Cap tulo 5
44
Todos los programas que se han visto hasta ahora tienen un ujo continuo desde arriba hasta abajo, es decir las instrucciones se ejecutan empezando en la primera l nea y descendiendo hasta la u ltima. Esto obliga a copiar parte del c odigo cuando se quiere que el programa repita una determinada tarea. Sin embargo el lenguaje C, como cualquier otro lenguaje de programaci on, proporciona maneras de modicar el ujo del programa permitiendo pasar varias veces por un conjunto de instrucciones. Se llama bucle de un programa a un conjunto de instrucciones que se repite varias veces. Es decir, es una zona del programa en la que el ujo deja de ser descendente y vuelve hacia atr as para repetir la ejecuci on de una serie de instrucciones. El bucle m as sencillo, que se introduce en este cap tulo, es el bucle for, que generalmente se utiliza para repetir parte del c odigo un cierto n umero de veces.
Este programa hace lo mismo que el programa anterior, pero podemos escribir el n umero de sobres que queramos simplemente cambiando el 3 de la l nea 8 por otro n umero. En este ejemplo la instrucci on i=0 es la sentencia inicial, que se ejecuta antes de entrar en el bucle. La condici on es i<3, que indica que el bucle se va a repetir mientras sea cierto que la variable i tiene un valor menor que 3. Como inicialmente se asigna el valor 0 a la variable i, inicialmente se cumple la condici on del bucle, por lo tanto el ujo del programa entra en el bucle y se ejecutan todas las sentencias comprendidas entre las dos llaves ( { en la l nea 8, y } en la l nea 12). Al alcanzar la l nea 12 se ejecuta la sentencia nal de bucle, en este caso i++ y luego se vuelve a comprobar la condici on. En la primera iteraci on la variable i vale 0, pero al llegar al nal del bucle la instrucci on i++ hace que pase a valer 1, entonces se vuelve a comprobar la
45
condici on i<3 que se cumple y por lo tanto el ujo vuelve a la l nea 9. Este bucle se ejecuta 3 veces en total, durante la primera pasada i vale 0, al llegar a la l nea 12 pasa a valer 1 y hace la segunda pasada, entonces pasa a valer 2 y hace la tercera. Al terminar la tercera pasada, la instrucci on i++ hace que i valga 3 y por lo tanto deja de cumplirse la condici on i<3 ya que 3 no es menor que 3. Como puede apreciarse, la variable i controla el n umero de veces que se ejecuta el bucle. Por ello a este tipo de variables se les denomina variables de control. Es muy importante hacer un buen uso del sangrado para facilitar la lectura del programa. En este ejemplo puede observarse que todas las instrucciones del programa tienen un sangrado de tres espacios, pero las instrucciones del bucle (l neas 9 a 11) tienen un sangrado adicional de otros tres espacios. De esta manera se ve claramente que la llave de la l nea 12 cierra el bucle for de la l nea 8 mientras que la llave de la l nea 14 cierra la funci on main haciendo pareja con la llave de la l nea 5. Puesto que el compilador ignora todos los espacios y los cambios de l nea, cada programador puede elegir su estilo propio, lo importante es ser coherente en todo el c odigo. Por ejemplo en lugar de 3 espacios pueden escribirse siempre 2, lo que no vale es poner unas veces 3 y otras 2 porque entonces las columnas salen hechas un churro. Tampoco es correcto imprimir los programas utilizando un tipo de letra proporcionado 1 como Times o Helv etica sino que los programas deben imprimirse en tipos de letra monoespaciados como por ejemplo Courier. Por u ltimo destacar las siguientes nociones: la sentencia inicial siempre se ejecuta la condici on siempre se comprueba antes de cada iteraci on. Por lo tanto puede ocurrir que nunca se llegue a entrar en el bucle si desde el principio no se cumple la condici on. Por ejemplo for(i=0; i<-3; i++) nunca entra en el bucle. la sentencia nal se ejecuta al terminar cada iteraci on. Por lo tanto si no se entra en el bucle esta sentencia no se ejecuta nunca.
46
No debe confundirse el operador de asignaci on = con el operador relacional de igualdad == que no modica el valor de las variables. Es decir, es totalmente distinto escribir i=7, que introduce el valor 7 en la variable i independientemente del valor que tuviese e sta, que escribir i==7, que compara el valor de la variable i con la constante 7. Ejercicio 1. Comprobar que otra forma de escribir el bucle del ejemplo ser a poner for(i=0; i!=3; i++).
47
En caso de bucles anidados es m as importante todav a tener cuidado con utilizar un sangrado correcto, de manera que se identique claramente el comienzo y el nal de cada bucle. En este ejemplo tambi en se puede observar que no es lo mismo escribir la instrucci on ColocarEnPosicion() antes del for interno que despu es de la llamada a BajarUnMetro(). Aunque parezca lo mismo, s olo se garantiza que el robot empieza a cortar el c esped en la direcci on adecuada cuando el programa est a escrito como en el ejemplo. Es importante tener cuidado con las inicializaciones.
Ejercicios
1. Escribir un programa que escriba con printf todos los n umeros del 0 al 20. 2. Modicar el programa anterior para que escriba s olo los n umeros pares del 2 al 20.
48
Cap tulo 6
bip bip
Como futuros ingenieros que somos no nos vamos a rendir f acilmente ante semejante problema. Una soluci on es colocar al robot un sensor que nos diga cuando toca la pared, y hacer avanzar el robot hasta que toque la pared, girar 90 grados, y seguir avanzando pegado a la pared, lo que seguir a siendo detectado por nuestro estupendo sensor, hasta que lleguemos a la puerta, momento en el cual giraremos -90 grados para salir, cumpliendo con e xito nuestro digo1 como: objetivo. El algoritmo descrito se puede expresar usando pseudoc o
1 Un
pseudoc odigo es una manera de expresar un algoritmo usando lenguaje natural y a un nivel de detalle muy
alto.
49
50
Como podemos apreciar, el algoritmo consiste en repetir una serie de instrucciones mientras una condici on es cierta. Para ello todos los lenguajes estructurados poseen unas sentencias de control espec cas, que el caso del C son los bucles while y do-while.
6.2.1. Ejemplos
Veamos a continuaci on algunos ejemplos del uso del bucle while. Sacar al robot de la habitaci on Se desea resolver en C el problema propuesto en la introducci on. El fabricante del robot, muy gentilmente, nos ha cedido varias rutinas en C para que su manejo sea m as f acil. Estas rutinas son: avanza 1 cm adelante() que hace avanzar al robot un cent metro hacia adelante. gira(double angulo) que es una funci on que admite un double como par ametro y que hace que el robot gire a derechas el n umero de grados indicado en dicho par ametro. toca pared() que es una funci on que vale uno si el robot toca la pared y cero si no la toca.
51
El ejemplo descrito mediante pseudoc odigo en la introducci on se realizar a en lenguaje C de la siguiente manera: /* Programa: SacaRob * * Descripci on: Saca al robot de una habitaci on como la mostrada en * la figura 6.1. * * Revisi on 0.0: 10/03/1998 * * Autor: El robotijero loco. */ #include <stdio.h> #include <robot.h> /* Declara las funciones del robot */ main(void) { while(toca_pared() != 1){ avanza_1_cm_adelante(); /* Avanza hacia la pared */ } girar(90); while(toca_pared() == 1){ avanza_1_cm_adelante(); /*Avanza pegado a la pared hacia la puerta*/ } girar(-90); /* Se pone delante de la puerta */
avanza_1_cm_adelante(); /* Sale por la puerta! */ } Suma de series. A nuestro hermanito peque no le han mandado en la escuela la ardua tarea realizar las sumas siguientes: 1
27
34
54
34
356
1234
34
2378
23
2345
Como nuestro hermanito peque no no tiene a un calculadora por haberse gastado el presupuesto familiar previsto para el caso en chicles, nos pide que le dejemos la nuestra. El problema es que se acaban de gastar las pilas y es domingo por la tarde, con lo que s olo tenemos dos opciones: hacer las sumas a mano o realizar un peque no programa en C, lo cual parece ser lo m as razonable, dado que existen fundadas expectativas de que el pr oximo d a de clase el malvado profesor de matem aticas vuelva a atormentar a nuestro hermanito con m as sumas enormes.
52
Como buenos programadores que somos ya a estas alturas, antes de encender el ordenador tomamos papel y l apiz para analizar el problema, el cual no es m as que sumar una serie de n umeros. Lo primero que se nos ocurre es realizar un programa que realice las sumas del problema de nuestro hermanito: soluci on f acil, pero poco apropiada, pues s olo es v alida para este ejercicio y no para los que se avecinan. Lo siguiente que se nos ocurre es realizar un bucle, que vaya pidiendo los n umeros por el teclado y los vaya sumando. El problema ahora es que el n umero de sumandos var a de una suma a otra. La soluci on a este problema es doble: podemos pedir al usuario al principio del programa el n umero de sumandos y con esta informaci on usar un bucle for; pero el inconveniente de esta soluci on es que hay que contar antes todos los n umeros, lo cual, aparte de ser un rollo, es propenso a errores. La segunda soluci on es usar un bucle while y decirle al usuario que despu es del u ltimo n umero introduzca un cero para indicar al programa el nal de la serie. Un posible pseudoc odigo ser a: pedir n umero; mientras n umero no sea cero { sumar el n umero a la suma anterior; pedir n umero; } imprimir total; De este pseudoc odigo lo u nico que nos queda por resolver es c omo hacer para sumar el n umero a la suma anterior. Este tipo de operaciones son muy comunes en C y se ndola a cero antes de entrar en el bucle y dentro resuelven creando una variable, inicializa del bucle se incrementa con el valor deseado, es decir: int suma; on */ suma = 0; /* Inicializaci bucle{ /* bucle for, while... */ suma += lo_que_sea; } hacer lo que sea con suma; En este pseudoc odigo primero se dene la variable suma como entera (obviamente el tipo en cada caso ser a el que haga falta). Despu es hay que inicializarla, pues dentro del bucle se hace suma += lo que sea que como recordar a es una manera abreviada de expresar suma = suma + lo que sea y por tanto si la variable suma no se inicializa antes de entrar en el bucle, el valor de dicha variable puede ser cualquier cosa, con lo que el valor de la suma nal ser a err oneo y el suspenso de nuestro hermanito cargar a para siempre sobre nuestras conciencias. Una vez resueltos todos los problemas de nuestro programa podemos pasar a escribirlo en C, quedando como: 01 /* Programa: SumaSer 02 * 03 * Descripci on: Suma una serie de n umeros introducidos por teclado. La 04 * serie se termina introduciendo un cero, momento en el 05 * que se imprime el total de la suma por pantalla. 06 * 07 * Revisi on 0.0: 10/03/1998
53
printf("Introduzca los n umeros a sumar. Para terminar teclee un 0\n"); suma = 0; /* inicializo el total de la suma */
scanf("%d",&numero); /* pido el primer n umero */ while(numero != 0){ suma += numero; scanf("%d",&numero); } printf("El valor de la suma es: %d\n", suma); }
6.3.1. Ejemplos
Como el movimiento se demuestra andando, vamos a ver un ejemplo del bucle do-while.
Vamos a resolver el problema de la suma de series de la secci on 6.2.1 usando un bucle do-while. En este caso, despu es de la fase de an alisis que se realiz o en dicha secci on llegamos a que otro posible pseudoc odigo que soluciona el mismo problema es: hacer{ pedir n umero; sumar el n umero a la suma anterior; }mientras n umero no sea cero; imprimir total; Que expresado en C resulta: 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 /* Programa: SumaSer * * Descripci on: Suma una serie de n umeros introducidos por teclado. La * serie se termina introduciendo un cero, momento en el * cual se imprime el total de la suma por pantalla * * Revisi on 1.0: 10/03/1998 * * Autor: El hermano del hermanito sin calculadora. */ #include <stdio.h> main(void) { int suma; /* Valor de la suma */ int numero; /* numero introducido por teclado */ printf("Introduzca los n umeros a sumar. Para terminar teclee un 0\n"); suma = 0; /* inicializo el total de la suma */ do{ scanf("%d",&numero); suma += numero; }while(numero != 0); printf("El valor de la suma es: %d\n", suma); }
Comparando esta soluci on con la anterior usando un bucle while, vemos que la u nica diferencia es que ahora no hace falta pedir el primer n umero antes de comenzar el bucle, pues e ste se ejecuta al menos una vez, comprob andose la condici on al nal. Por tanto, es posible leer el n umero dentro del bucle y comprobar si es cero al nal de e ste. Otra cosa que cambia es el orden de las dos instrucciones que hay dentro del bucle. Se deja como ejercicio la explicaci on del porqu e de semejante cambio.
55
56 }
nn
pedir valor final n; inicializar suma total; for(i=1; i<=n; i++){ sumar i a la suma total; } imprimir suma total; calcular el valor de la serie e imprimirla; Una vez comprobado que el pseudoc odigo realiza lo que queremos pasamos a la acci on, codicando el siguiente programa: 1 /* Programa: SerAri 2 * 3 * Descripci on: Suma una serie aritm etica de 1 a n, siendo n un 4 * dato introducido por el usuario, imprimiendo el 5 * valor de la suma y el de la formula n*(n+1)/2. 6 * 7 * Revisi on 0.0: 10/03/1998 8 *
6.6. EJERCICIOS
9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 * Autor: El alumno charlat an. */ #include <stdio.h> main(void) { int suma; /* Valor de la suma */ int numero; /* Valor final de la serie */ int i; /* ndice del for */ printf("Introduzca el n umero final de la serie.\n"); scanf("%d", &numero); suma = 0; /* inicializo el total de la suma */
57
for(i=1; i<=numero; i++){ suma += i; } printf("El valor de la suma es: %d\n", suma); printf("Y el de la f ormula es: %d\n", numero*(numero+1)/2 ); }
En este programa se han introducido dos novedades: la primera ha sido la anunciada previamente de usar la variable de control del bucle i dentro del bucle (l nea 26). La segunda es la de usar una expresi on dentro del printf en la linea 30. El funcionamiento de esta instrucci on es el siguiente: se eval ua en primer lugar la expresi on numero*(numero+1)/2 y el valor resultante es el que se imprime en pantalla.
6.6. Ejercicios
1. Realizar un programa para calcular multiplicaciones del tipo: 2 * 3 * 35 345 * 34 * 3.5 * 23 * 2.1 2 * 2.234 Hacer dos versiones: una en la que se pida al usuario al principio del programa la cantidad de n umeros que desea multiplicar para pasar seguidamente a pedirle cada uno de los n umeros a multiplicar. En la otra versi on el usuario introducir a uno a uno los n umeros, indic andole al programa que ya no hay m as mediante la introducci on de un 1. 2. Realizar un programa para comprobar la veracidad de la siguiente f ormula para calcular la suma de una serie geom etrica: 1
n2
Pistas: La instrucci on nal de los bucles for puede ser tambi en i+=2. Para calcular n2 se puede hacer n*n
58
Cap tulo 7
Control de ujo: if
7.1. Introducci on
Es habitual que los programas realicen tareas diferentes en funci on del valor de determinadas variables. La instrucci on if permite denir bloques de c odigo que no son de ejecuci on obligatoria y por lo tanto son bloques de instrucciones que el programa se puede saltar. Esta decisi on depende del valor de una condici on l ogica, que a su vez suele depender del valor que adquieran determinadas variables del programa. Tambi en es posible denir dos bloques de instrucciones alternativos, de manera que s olo se ejecute uno u otro en funci on del resultado de la condici on l ogica.
60 instrucci on 2.m; }
Veamos como ejemplo un programa que pregunta el valor de dos n umeros enteros y que escribe en pantalla cu al es el mayor de los dos. Este programa debe utilizar scanf para preguntar los valores y luego un if para decidir si se escribe el primero o el segundo. /********************* Programa: Compara.c Descripci on: Lee dos n umeros del teclado y los compara on 0.0: 16/FEB/1999 Revisi Autor: Rafael Palacios *********************/ #include <stdio.h> main(void) { int a,b; printf("Dame un valor entero "); scanf("%d",&a); printf("Dame otro valor "); scanf("%d",&b); if (a>b){ printf("El mayor es %d\n",a); } else { printf("El mayor es %d\n",b); } } Otra aplicaci on muy interesante es evitar errores de c alculo en los programas, como por ejemplo las divisiones por cero o las ra ces cuadradas de n umeros negativos. El siguiente ejemplo es un programa que incluye un control para evitar las divisiones por cero. En realidad el ejemplo parece algo tonto porque en caso de error simplemente termina con un exit, pero un programa m as completo puede tomar otras medidas como por ejemplo volver a preguntar el valor del denominador. La funci on exit() hace que el programa termine inmediatamente y le devuelve al sistema operativo el valor de su argumento1. Esta manera de terminar un programa antes de que llegue lo est la ejecuci on al nal de la funci on main, so a permitida en caso de detectarse un error que impida continuar con la ejecuci on normal del programa. /********************* Programa: Division.c Descripci on: Calcula el cociente de dos n umeros. El programa incluye un control que evita dividir por cero. Revisi on 0.0: 16/FEB/1999
1 Por convenio, un programa que termina a causa de un error debe devolver al sistema operativo un valor distinto de cero.
61
62
operadores l ogicos y por lo tanto condiciones como la siguiente se eval uan realizando primero los c alculos y luego las comparaciones: (a > 3*b) (a+b == 0) /* Esto hace ( a > (3*b) ) */ /* Esto hace ( (a+b) == 0 ) */
En caso de duda lo mejor es poner los par entesis necesarios antes que perder el tiempo haciendo pruebas en el programa. Es decir, o se mira la documentaci on y se hacen las cosas bien a la primera o se ponen par entesis hasta que la operaci on quede clara. Esto u ltimo tiene la ventaja de que estar a claro para todo el que lea el programa, aunque no se acuerde muy bien de las precedencias de los operadores. Por u ltimo conviene destacar que en C los operadores relacionales son operadores binarios, es decir, realizan la comparaci on entre los operandos que est an a su derecha y a su izquierda; siendo el resultado de dicha comparaci on un 1 si la condici on es cierta o un 0 si es falsa. Si, acostumbrados a la notaci on matem atica, se escribe: 0 <= a <= 10 para expresar la condici on 0 a 10, el resultado de dicha expresi on ser a siempre 1, independientemente del valor de la variable a. Puede adivinar por qu e? Si se tiene en cuenta que en C las expresiones se eval uan de izquierda a derecha, en primer lugar se evaluar a el primer a a su izquierda (0) con el que est a a su operador <=, el cual compara el operando que est derecha (a). El resultado de esta expresi on ser a 0 si a es menor que 0 o 1 en caso contrario. Dicho resultado ser a el operando izquierdo del siguiente operador relacional, con lo que se un haya sido el resultado de la comparaci on evaluar a la expresi on 0 <= 10 o 1 <= 10 seg anterior. En cualquier caso el resultado nal de la expresi on ser a cierto (1). Para expresar en C la condici on 0 a 10 correctamente, es necesario dividir la relaci on en dos, es decir, en primer lugar comprobar si a es mayor o igual que 0 y en segundo lugar si a es menor o igual que 10. Por u ltimo es necesario usar un operador l ogico para relacionar ambas expresiones, de forma que el resultado sea cierto cuando ambas condiciones sean ciertas. Se atreve a escribir la condici on? Por si acaso no se ha atrevido, e sta se muestra a continuaci on:
63
if (altura<1.5) { printf("Usted es bajito\n"); } else { if (altura<1.7) { printf("Usted no es alto\n"); } else { if (altura<1.9) { printf("Usted es alto\n"); } else { printf("Juega al baloncesto?\n"); } } } printf("Fin del programa.\n"); } En este tipo de estructuras de programa hay que ser muy cuidadoso para colocar correctamente las llaves y es imprescindible hacer un buen uso del sangrado para facilitar la lectura del programa. Unicamente remarcar que el siguiente programa aunque es mucho m as compacto no hace lo mismo que el anterior: /********************* Programa: Alturas2.c Descripci on: Pregunta la altura de una persona on 1.0: 16/FEB/1999 Revisi Autor: Rafael Palacios *********************/ #include <stdio.h> main(void) { double altura; printf("Cu al es su altura? "); scanf("%lf",&altura); if (altura<1.5){ printf("Usted es bajito\n"); } if (altura<1.7){
65
Este programa es incorrecto porque por ejemplo para alturas de 1.4 m se escriben tres mensajes en lugar de uno, ya que este valor cumple las tres condiciones de los tres if. En el caso del primer programa, al ser cierto el primer if, se escribe Es usted bajito y se salta directamente al u ltimo printf del programa porque el resto de las instrucciones forman parte del else del primer if. Para este tipo de situaciones se puede usar el bloque if else-if, cuya sintaxis es: if (condici on) { instrucci on 1.1; instrucci on 1.2; ... instrucci on 1.m; } else if (condici on) { instrucci on 2.1; instrucci on 2.2; ... instrucci on 2.n; ... } else { instrucci on 3.1; instrucci on 3.2; ... instrucci on 3.p; } El funcionamiento de esta construcci on es el siguiente: Se eval ua la condici o n del primer if. Si es cierta se ejecuta el bloque perteneciente a este if. Si es falsa se eval ua la condici o n del else if que le sigue, ejecut andose su bloque si dicha condici o n es cierta o pasando al siguiente else if si es falsa. Si ninguna de las condiciones es cierta se ejecutar a el bloque del u ltimo else en caso de que exista, pues es opcional su uso. Usando el bloque if else-if el programa anterior se puede escribir, de una manera mucho m as clara como: /********************* Programa: Alturas3.c Descripci on: Pregunta la altura de una persona Revisi on 0.0: 16/FEB/1999 Autor: Rafael Palacios *********************/ #include <stdio.h>
66
main(void) { double altura; printf("Cu al es su altura? "); scanf("%lf",&altura); if (altura<1.5){ printf("Usted es bajito\n"); }else if (altura<1.7){ printf("Usted no es alto\n"); }else if (altura<1.9){ printf("Usted es alto\n"); }else{ printf("Juega al baloncesto?\n"); } printf("Fin del programa.\n"); }
7.6. Ejercicios
1. Repetir el programa anterior pero sin utilizar else. Es decir, preguntando en el mismo if si altura es menor que 1.7 y mayor o igual a 1.5. Pista: Hay que utilizar los operadores
l ogicos.
Cap tulo 8
68
aunque lo m as c omodo es un n umero o una letra: por ejemplo 1 signicar a avanzar hacia adelante y 2 girar un n umero de grados (que habr a que preguntarle previamente al usuario). Un posible pseudoc odigo del programa ser a: pedir comando al usuario; si es 1: avanza_1_cm_adelante(); si es 2: pedir n umero de grados; gira(n umero de grados); Este tipo de situaciones son muy comunes en todos los programas. Por ello, aunque este tipo de decisiones se podr an resolver con sentencias if de la forma: if(comando == 1){ avanza_1_cm_adelante(); }else if (comando == 2){ pedir n umero de grados; gira(n umero de grados); } algunos lenguajes poseen estructuras de control espec cas para resolver m as f acilmente este tipo de decisiones. En el caso de C esta estructura de control es la construcci on switch-case.
69
S olo hay dos llaves: la de despu es del switch y la del nal. Los bloques de instrucciones pertenecientes a cada uno de los case vienen delimitados por el case y por el break del nal. Una vez advertido esto, pasemos a describir el funcionamiento de esta construcci on. En primer lugar se eval ua la expresi on que sigue al switch, con lo que se obtendr a un valor que ha de ser entero. Este valor se compara entonces con la constante entera que sigue al primer case (la constante 1). Si el resultado de la comparaci on es falso se prueba suerte con el siguiente case y as sucesivamente hasta encontrar alg un case cuya constante sea igual al resultado de la expresi on. Si esto ocurre se ejecutar an las instrucciones situadas entre el afortunado case y el break que naliza su bloque de instrucciones, continuando despu es con las instrucciones que sigan a la llave que cierra el switch. Si ning un case tiene a su lado una constante que coincida con el valor de la expresio an las instrucciones situadas entre n, entonces se ejecutar el default y el u ltimo break, y luego se continuar a con las instrucciones que siguen despu es del switch. Conviene hacer notar que se puede escribir un bloque switch-case sin el apartado de default. Si esto ocurre y se da el desafortunado caso de que ninguno de los case contenga el valor de la constante resultante de la evaluaci on de la expresi o on del n, entonces la ejecuci programa contin ua tranquilamente despu es de la llave que cierra el switch-case. Para aclarar ideas, veamos el siguiente ejemplo de un switch-case: switch (a){ case 1: printf("a vale uno\n"); break; case 2: printf("a vale dos\n"); break; default: printf("a no vale ni uno ni dos\n"); break; } Supongamos que a vale 1. Como acabamos de decir, en primer lugar se eval ua la expresi on que hay entre los par entesis situados despu es del switch. En este caso la expresi on es simplemente una variable, por lo que el resultado de la expresi on ser a el valor de la variable, es decir un 1. El programa compara entonces el valor as obtenido con el que hay a continuaci on del primer case, que como hemos tenido mucha suerte, es tambi en un 1, con lo que se ejecutar a el conjunto de instrucciones situadas entre este case y el siguiente break que en este ejemplo tan simple consta tan solo de la instrucci on printf("a vale uno\n").
8.2.3. Ejemplos
Veamos a continuaci on algunos ejemplos para ilustrar el uso de la construcci on switch-case. El robot con control manual Continuando con el ejemplo presentado en la introducci on de este cap tulo (apartado 8.2.1) el pseudoc odigo del programa se puede traducir a C usando un switch-case de la siguiente manera:
70 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
Ahora bien, salvo que el usuario tenga muy buena memoria, no se acordar a para siempre de los comandos, por lo que cada vez que tenga que ejecutar el programa tendr a que consultar el manual de usuario para ver qu e comandos tiene disponibles. Para evitar semejante engorro la soluci on es bien sencilla: imprimamos al comienzo del programa una lista de los comandos disponibles, junto con una breve descripci on de lo que hacen. Haciendo esto, el programa al arrancar podr a mostrar por pantalla lo siguiente: Control manual del robot. Men u de opciones: 1.- Avanzar 1 cm hacia adelante. 2.- Girar. Introduzca opci on:
71
A esto se le conoce con el nombre de menu de opciones, dado su parecido con los men us de los restaurantes (aunque presentan el grave inconveniente de que no se pueden comer). Para conseguir esto, basta con a nadir al programa anterior entre sus l neas 19 y 20 las instrucciones: printf(" printf(" printf(" printf(" Control manual del robot.\n\n"); Men u de opciones:\n"); 1.- Avanzar 1 cm hacia adelante.\n"); 2.- Girar.\n\n");
Un ejemplo de sesi on con el programa, suponiendo que dicho programa se llama robman, ser a: u:\c\grupo00\robman> robman Control manual del robot. u de opciones: Men 1.- Avanzar 1 cm hacia adelante. 2.- Girar. Introduzca opci on: 1 u:\c\grupo00\robman> robman Control manual del robot. u de opciones: Men 1.- Avanzar 1 cm hacia adelante. 2.- Girar. on: 2 Introduzca opci Cuantos grados desea girar: 27 u:\c\grupo00\robman> Como podemos apreciar el manejo del robot as es un poco pesado, pues para cada instrucci on que deseemos darle hemos de volver a llamar al programa. Para hacer un poco m as f acil la vida del manejador del robot, podemos realizar un bucle que englobe a todo el programa anterior. Un posible pseudoc odigo de esta soluci on ser a: repetir{ u imprimir men pedir comando al usuario; si es 1: avanza_1_cm_adelante(); si es 2: umero de grados; pedir n umero de grados); gira(n }mientras el usuario quiera; El problema ahora est a en como descubrir cuando el usuario quiere abandonar el programa. Obviamente la u nica manera de hacerlo (hasta que no se inventen los ordenadores que lean el pensamiento) es pregunt arselo directamente. Para ello nada m as f acil que incluir una nueva opci on en el men u de forma que cuando se elija se cambie la variable l ogica que controla el bucle. Un posible pseudoc odigo en el que se usa la opci on 3 para salir del programa ser a:
72
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
/* Programa: RobMan * * Descripci on: Controla el robot de forma manual. Para ello espera * a que el usuario introduzca su elecci on y ejecuta * entonces la opci on solicitada por el usuario. * * Revisi on 0.1: 16/03/1998 u de opciones y se introduce un * Se imprime un men * bucle para repetir el programa. * on 0.0: 15/03/1998 * Revisi * * Autor: El robotijero loco. */ #include <stdio.h> #include <robot.h> main(void) { int comando; /* Contendr a el comando introducido por el usuario */ float grados; /* Grados a girar por el robot*/ int salir; /* Variable l ogica que pondremos a 1 cuando el usuario desee salir del programa */ salir = 0; /* inicializamos la variable de control para que el bucle se repita hasta que se pulse la opci on 3.*/ do{ printf(" Control manual del robot.\n\n"); printf(" Men u de opciones:\n"); printf(" 1.- Avanzar 1 cm hacia adelante.\n"); printf(" 2.- Girar.\n"); printf(" 3.- Salir del programa.\n\n"); printf("Introduzca la opci on: "); scanf("%d", &comando);
73
2. Comprobar el funcionamiento del programa escrito en el ejercicio anterior en un ordenador. Como no disponemos del robot ni de las funciones para manejarlo, sustituir las llamadas a las funciones del robot por printfs que indiquen la llamada a la funci on correspondiente para poder comprobar f acilmente el correcto funcionamiento de nuestro programa.
74
En este ejemplo si letra vale A el programa salta a la instrucci on que sigue a case A: y continuar a ejecut andose hasta que encuentre un break o hasta que llegue el nal del switch. Por tanto si se da este caso (letra vale A) se ejecutar a printf("Opci on a del men u\n") y despu es se saldr a del switch. Conviene destacar que la sentencia case a: es una etiqueta del switch-case, no una instrucci on ejecutable.
8.3.3. Ejemplos
Tabla de logaritmos Se desea imprimir una tabla de logaritmos desde un valor inicial hasta un valor nal, en pasos decrecientes de 0.1. El usuario introducir a el valor inicial y luego el valor nal (menor que el inicial). El programa imprimir a entonces la tabla de logaritmos. La soluci on obvia a este problema es el uso de un bucle for desde el valor inicial hasta el nal. El problema de
75
esta soluci on es que si el usuario no sabe mucho de matem aticas, puede introducir un valor nal negativo, con lo que el programa terminar a de una forma poco amigable. Para evitar que se calculen logaritmos de n umeros menores o iguales a cero, podemos interrumpir el bucle con una sentencia break si detectamos que el bucle llega a un n umero menor o igual a cero. El programa escrito en C quedar a: /* Programa: CalcLog * * Descripci on: Calcula una tabla de logaritmos desde un valor inicial * introducido por el usuario hasta otro final, tambi en * introducido por el usuario y menor que el anterior; en * pasos de 0.1. * * Revisi on 0.0: 24/03/1998 * * Autor: El tablero aplicado. */ #include <stdio.h> #include <stdlib.h> #include <math.h> main(void) { double val_inic; /* Valor inicial */ double val_fin; /* Valor final */ double num; /* n umero usado para recorrer la tabla*/ printf("Valor Inicial = "); scanf("%lf",&val_inic); printf("Valor Final = "); scanf("%lf",&val_fin); /* imprimo una cabecera */ printf("\n numero logaritmo\n"); for(num = val_inic; num >= val_fin; num -= 0.1){ if(num <= 0.0){ break; } printf("%7.3f %7.3f\n", num, log(num) ); } } Un ejemplo de una sesi on con el programa muestra su correcto funcionamiento: Valor Incial = 1.01 Valor Final = -1.01 numero logaritmo
76 1.010 0.910 0.810 0.710 0.610 0.510 0.410 0.310 0.210 0.110 0.010
En la que se aprecia que el bucle se interrumpe cuando num se hace igual a -0.09 gracias a la sentencia break, terminando el programa.
77
Como se puede observar el ujo del programa est a mucho m as claro en este caso, pues la ejecuci on es siempre desde el principio del bucle hasta el nal, sin saltos raros por el medio.
8.4.1. Ejemplos
Tabla de senos, cosenos, logaritmos Se desea confeccionar una tabla de senos, cosenos y logaritmos (naturales y en base 10) para los n umeros comprendidos entre y espaciados a intervalos de 0.1. Para confeccionar esta tabla lo natural es realizar un bucle for desde hasta que calcule las funciones y las imprima. El problema que se nos presenta es que los logaritmos no est an denidos para n umeros menores o iguales a cero. Una posible soluci on a este problema es colocar estas dos operaciones al nal del bucle y usar la sentencia continue para saltarnos el nal del bucle siempre que el operando sea menor o igual que cero. Una posible implantaci on del programa en C podr a ser:
/* Programa: CalcTabla * * Descripci on: Calcula una tabla de senos, cosenos y logaritmos en * base natural y decimal para los n umeros comprendidos * entre -pi y pi con intervalos de 0.1 * * Revisi on 0.0: 24/03/1998 * * Autor: El tablero aplicado. */ #include <stdio.h> #include <stdlib.h> #include <math.h> main(void) { double num; /* n umero usado para recorrer la tabla*/ /* imprimo una cabecera */ printf("\n numero seno coseno
logn
log10\n");
for(num = -PI; num <= PI; num += 0.1){ /* el seno y el coseno se calculan siempre */ printf("%7.3f %7.3f %7.3f ", num, sin(num), cos(num) ); if(num <= 0.0){ printf("\n");/* si no calculo los logaritmos he de dar el salto de carro que se dar a en su printf */ continue; } printf("%7.3f %7.3f\n", log(num), log10(num)); } }
78
numero seno coseno logn log10 -3.142 -0.000 -1.000 -3.042 -0.100 -0.995 -2.942 -0.199 -0.980 -2.842 -0.296 -0.955 ........................................ -0.342 -0.335 0.942 -0.242 -0.239 0.971 -0.142 -0.141 0.990 -0.042 -0.042 0.999 0.058 0.058 0.998 -2.840 -1.234 0.158 0.158 0.987 -1.843 -0.800 0.258 0.256 0.967 -1.353 -0.588 0.358 0.351 0.936 -1.026 -0.446 ........................................ 2.758 0.374 -0.927 1.015 0.441 2.858 0.279 -0.960 1.050 0.456 2.958 0.182 -0.983 1.085 0.471 3.058 0.083 -0.997 1.118 0.485 En donde se han eliminado l neas intermedias para ahorrar papel y no aburrir demasiado al lector. Este mismo ejemplo se podr a haber codicado sin usar continue de la siguiente manera: /* Programa: CalcTabla * * Descripci on: Calcula una tabla de senos, cosenos y logaritmos en * base natural y decimal para los n umeros comprendidos * entre -pi y pi con intervalos de 0.1 * * Revisi on 0.1: 24/03/1998 * No se hace uso ahora de la sentencia continue. on 0.0: 24/03/1998 * Revisi * * Autor: El tablero aplicado. */ #include <stdio.h> #include <stdlib.h> #include <math.h> main(void) { double num; /* n umero usado para recorrer la tabla*/ /* imprimo una cabecera */ printf("\n numero seno coseno
logn
log10\n");
8.5. EJERCICIOS
/* el seno y el coseno se calculan siempre */ printf("%7.3f %7.3f %7.3f ", num, sin(num), cos(num) ); if(num > 0.0){ printf("%7.3f %7.3f", log(num), log10(num)); } printf("\n"); /* Imprimo el final de la linea de la tabla */ } }
79
Que como se puede apreciar es m as f acil de entender que el anterior. N otese tambi en que la condici on del if ahora es distinta. Por qu e?
8.5. Ejercicios
1. Modicar el programa CalcLog presentado en la secci on 8.3.3 de forma que se compruebe que el usuario introduce siempre un valor inicial mayor que el valor nal, dando un error en caso contrario y saliendo del programa. 2. Modicar el programa anterior para que cuando el usuario introduzca un valor err oneo se le avise con un mensaje de error y se le vuelva a pedir de nuevo el valor nal. 3. Realizar el programa para controlar el robot de forma interactiva presentado en la secci on 8.2.3 pero usando un men u con letras, ya que el futuro usuario ha mostrado una profunda alergia a los n umeros, maniesta con dolores de cabeza, congesti on nasal y escozores en la piel. 4. Escribir el programa anterior en el ordenador. Funciona correctamente?. En caso de que no funcione correctamente Qu e funcionamiento an omalo presenta?. Si el funcionamiento an omalo es que despu es de introducir una opci on nos la ejecuta y luego sin que el usuario haga nada m as, el programa dice el solito que se ha introducido una opci on err onea a qu e puede ser debido esto? (Pista: al introducir una opci on despu es se da al intro, que el ordenador interpreta como un car acter m as llamado \n). Como se podr a solucionar este problema tan desagradable? (Esto u ltimo es para nota).
80
Cap tulo 9
Vectores y Matrices
9.1. Introducci on
En el cap tulo 3 se han descrito los tipos de datos b asicos que permiten declarar variables. Pero estos tipos de datos s olo permiten construir variables que almacenen un u nico valor. En este cap tulo se ve c omo denir variables capaces de almacenar m as de un valor, lo que nos permite trabajar con mayor comodidad con grandes conjuntos de datos. Estas variables se utilizan principalmente para trabajar con vectores, cadenas de caracteres y matrices.
9.2. Vectores
Un vector es un conjunto de datos del mismo tipo que se almacenan en el ordenador en posiciones de memoria consecutivas y a los cuales se accede mediante un mismo nombre de variable. La caracter stica fundamental es que todos los datos de un vector son del mismo tipo, por ejemplo todos son int o todos son double. La denici on de vectores es parecida a la denici on de variables, salvo que se debe especicar el tama no del vector entre corchetes [ ]. Por ejemplo, para denir un vector llamado vec que pueda almacenar 10 valores tipo double se escribe: double vec[10]; La manera de acceder a los valores de estas variables es ahora diferente, ya que de todos los elementos que componen el vector es necesario especicar cu al queremos modicar. Esta especicaci on se realiza indicando entre corchetes el n umero de orden del elemento, teniendo en cuenta que la numeraci on de los elementos siempre empieza en cero 1 . vec[0]=54.23; vec=4.5; vec[10]=98.5; /*Primer elemento del vector*/ /* ERROR */ /* ERROR */
La segunda asignaci on no es correcta porque vec no es una variable tipo double sino un vector y por lo tanto todas las asignaciones deben indicar el n umero de orden del elemento al que queremos acceder. El vector vec del ejemplo tiene tama no 10 porque fue declarado como
1 En matem aticas el primer elemento de los vectores suele ser el elemento 1 y por lo tanto en el vector a se habla de a1 como primer elemento y en la matriz A se habla de A1 1
81
82
double vec[10] esto signica que almacena 10 elementos. Los 10 elementos se numeran desde el 0 hasta el 9 y por lo tanto, el elemento 10 no existe y la tercera asignaci on tampoco es v alida. Es importante destacar que el compilador no dar a error en el u ltimo caso ya que no comprueba los rangos de los vectores. Este tipo de asignaci on hace que el valor 98.5 se escriba fuera de la zona de memoria correspondiente al vector vec, y probablemente machaca los valores de otras variables del programa. Hay que prestar especial atenci on ya que s olo en algunos casos aparece un error de ejecuci on (generalmente cuando el ndice del vector es muy grande) pero en otras ocasiones no aparece ning un mensaje de error y el programa simplemente funciona mal. Como se puede sospechar, la mejor manera de trabajar con vectores es utilizando bucles for. El siguiente ejemplo declara un vector de tama no 10, lo inicializa d andole valores crecientes y luego escribe todo el contenido del vector.
/********************* Programa: vector.c Descripci on: Inicializa un vector de enteros y luego escribe su contenido por pantalla. *********************/ #include <stdio.h> main(void) { double x[10]; int i; /* inicializaci on */ for(i=0; i<10; i++) { x[i]=2*i; } /* imprimir valores */ for(i=0; i<10; i++) { printf("El elemento %d vale %4.1f\n",i,x[i]); } } --- SALIDA -------------------El elemento 0 vale 0.0 El elemento 1 vale 2.0 El elemento 2 vale 4.0 El elemento 3 vale 6.0 El elemento 4 vale 8.0 El elemento 5 vale 10.0 El elemento 6 vale 12.0 El elemento 7 vale 14.0 El elemento 8 vale 16.0 El elemento 9 vale 18.0
83
84 Hola!@#$%&
Las cadenas de caracteres suelen tener una longitud variable; es decir, aunque tengamos memoria reservada para almacenar nombres de 10 caracteres unas veces tendremos nombres largos y otras veces tendremos nombres cortos. A la hora de imprimir, copiar o comparar estos nombres es necesario conocer su longitud.
85
La longitud m axima de la cadena de caracteres es 127, ya que el valor m aximo que puede almacenarse en la variable nombre[0] es 127 por ser de tipo char. La segunda manera de manejar cadenas de caracteres de longitud variable es utilizar un car acter especial para indicar el nal de la cadena. Este es el m etodo utilizando normalmente en el lenguaje C y existen un mont on de funciones dentro de la biblioteca est andar de C para manejar cadenas de caracteres basadas en este m etodo. Las caracter sticas de este m etodo son: En un vector de tama no n s olo caben n ocupa una posici on.
No existe l mite en la longitud de la cadena, el u nico l mite viene dado por el tama no del vector. Si se dene un vector de tama no 1000, como por ejemplo char nombre[1000], se pueden almacenar sin ning un problema hasta 999 caracteres. En cada posici on de la cadena se puede almacenar cualquier car acter, a excepci on del car acter denido como nal de cadena. Dado que existe un car acter que indica el nal de la cadena, no es posible almacenar en ella cualquier tipo de informaci on ya que dicho car acter no puede formar parte de la cadena. Para evitar el mayor n umero de problemas, en el lenguaje C se ha elegido como car acter de nal de cadena el car acter n umero 0 de la tabla ASCII (tambi en llamado NULL o car acter nulo y representado mediante \0). Existen muchos caracteres de uso poco frecuente, como por ejemplo el car acter %, que podr an haberse utilizado para marcar el nal de la cadena. Sin embargo estos caracteres dar an problemas antes o despu es. Si por ejemplo se desea realizar un programa para imprimir octavillas con la frase 0,7 % YA para pedir mayor justicia en este mundo enfermo en el que el 20 % de la poblaci on de los paises del norte posee el 80 % de la riqueza, el ordenador s olo imprmir a 0,7, lo cual perturbar a un poco el mensaje original. El car acter seleccionado en C, \0, no da problemas porque nunca se utiliza como parte de una cadena de caracteres, ya que no es un car acter imprimible. Usando esta t ecnica, el programa anterior puede escribirse de la siguiente manera: /********************* Programa: cadenas3.c Descripci on: Inicializa un vector de char y luego escribe el contenido del vector caracter por caracter hasta llegar al caracter NULL. on 0.0: 14/ABR/1998 Revisi Autor: Rafael Palacios *********************/ #include <stdio.h> main(void) { char nombre[10]; int i; /* inicializaci on */ nombre[0]=H; nombre[1]=o; nombre[2]=l; nombre[3]=a;
Hay que destacar que este bucle for escribe los caracteres en pantalla siempre que e stos sean sean diferentes del car acter NULL. Cuando el contador i alcance la posici on en la que as este bucle est a almacenado el car acter NULL, el bucle termina sin hacer el printf. Adem es v alido incluso si la cadena de caracteres est a vac a; es decir, si el primer car acter es directamente el car acter NULL entonces el bucle no arranca y no se escribe nada. Otra manera de escribir este bucle, pero utilizando while en lugar de for es la siguiente: i=0; while(nombre[i] != NULL) { printf("%c",nombre[i]); i++; } Por haber elegido el car acter 0 como indicador de nal de cadena, y gracias a que las condiciones de los bucles s olo se consideran falsas cuando valen 0, la condici on del while puede ser el propio car acter nombre[i]. Lo mismo ocurre en el ejemplo del bucle for, donde la condici on tambi en puede ser nombre[i] en lugar de nombre[i]!=\0. Sin embargo el c odigo resultante queda menos claro.
87
N otese que en la llamada a strlen se pone directamente el nombre de la variable (sin corchetes []). Este programa es equivalente al siguiente, que no utiliza strlen. /********************* Programa: cadenas5.c Descripci on: Inicializa un vector de char y luego escribe el contenido del vector caracter por caracter. Calcula la longitud de la cadena de caracteres mediante un bucle while. Revisi on 0.0: 14/ABR/1998 Autor: Rafael Palacios *********************/ #include <stdio.h> main(void) { char nombre[10];
88 int i; int longitud; /* inicializaci on */ nombre[0]=H; nombre[1]=o; nombre[2]=l; nombre[3]=a; nombre[4]=\0; /* calculo la longitud */ longitud=0; while(nombre[longitud] != 0) longitud++; } /* imprimir valores */ for(i=0; i<longitud; i++) { printf("%c",nombre[i]); } printf("\n"); } Copiar o inicializar: strcpy(), string copy
Esta funci on copia cadenas de caracteres. Es importante recordar que no es v alido asignar una constante a un vector y por lo tanto las siguientes instrucciones son incorrectas: vec=4.5; /* Error si vec es vector*/ nombre="Pepito"; /* Error */ Esta funci on no verica que el tama no del vector de destino sea sucientemente grande para guardar la cadena que se quiere copiar, por lo que el programador ser a responsable de garantizar que esto ocurra para no producir fallos indeseables en el programa. El formato de la llamada a strcpy es el siguiente: strcpy(destino, origen); es decir, tiene una sintaxis equivalente a la asignaci on normal de variables, del tipo: destino=origen; Esta funci on es muy u til para inicializar cadenas de caracteres, poniendo una constante entre comillas dobles como argumento origen. Esta inicializaci on evita tener que denir car acter por car acter el vector. Los siguientes principios de programa son equivalentes: #include <string.h> main(void) { char nombre[10];
89
: : :
Es importante destacar que la segunda versi on incluye el archivo string.h para utilizar la funci on strcpy y que no hace falta escribir el car acter \0 porque est a impl cito en todas las cadenas de caracteres constantes (aquellas que se escriben directamente en el c odigo encerradas entre comillas). El siguiente programa explica el comportamiento de la funci on strcpy para copiar la variable a en la variable dir (ambas son vectores de char de tama no suciente). for(i=0; a[i]!=\0; i++) { dir[i]=a[i]; } dir[i]=a[i]; /* Copio el NULL */ Este bucle for copia car acter a car acter la variable a en la variable dir, borrando la informaci on que pudiese tener previamente. La u ltima l nea de este c odigo es fundamental, ya que garantiza que la variable dir tenga car acter de nal de cadena. Cuando a[0] vale \0 el bucle no arranca y la u ltima l nea hace dir[0]=a[0] por lo tanto copia el car acter NULL. Cuando la cadena a no est a vac a, el bucle copia car acter por car acter todas las letras hasta llegar a NULL (car acter que no copia) entonces la u ltima l nea copia el NULL en su sitio. Comparaci on de cadenas: strcmp(), string compare Esta funci on compara cadenas de caracteres. Principalmente se utiliza para ver si dos nombres son iguales o para ordenarlos alfab eticamente. La funci on devuelve un valor de tipo int que puede ser cero, negativo o positivo. Por ejemplo la comparaci on de las variables nombre1 y nombre2 puede dar los siguientes resultados: i=strcmp(nombre1,nombre2); La variable entera i tomar a los siguientes valores: i=0 si nombre1 es igual a nombre2. Esto signica que todos los caracteres del vector nombre1 desde el ndice 0 hasta el car acter NULL (incluyendo a e ste) son iguales a los correspondientes caracteres del vector nombre2. Pero puede ocurrir que el resto de los caracteres (desde el car acter NULL hasta el nal del vector) sean diferentes, lo cual da lo mismo, ya que estos caracteres no forman parte de la cadena. Tambi en puede ocurrir que los vectores sean de diferente tama no, ya que s olo se comparan los caracteres anteriores a NULL. i<0 si nombre1 es menor que nombre2, entendiendo que nombre1 se ordena alfab eticamente por delante de nombre2. En este caso hay que tener en cuenta que la ordenaci on sigue los criterios de la tabla ASCII; es decir los n umeros son menores que las letras, las letras may usculas son menores que las min usculas y las letras acentuadas son mayores
90
Ejercicios 1. Comprobar el funcionamiento del programa para distintos ejemplos. Comprobar si funciona para palabras como fulmina y fulminante, alumno y alumna, etc. Da lo mismo en qu e variables (a o b) se almacenen las palabras? Visualizaci on de cadenas de caracteres La funci on printf puede utilizarse para visualizar cadenas de caracteres, no hace falta ninguna otra funci on especial. Cuando se pone el c odigo %s dentro de la especicaci on de formato, la funci on printf se encarga de escribir todos los caracteres del vector de char hasta encontrar el car acter NULL. i=0; while(nombre[i]) { printf("%c",nombre[i]); } printf("\n\n"); El programa anterior es equivalente a la instrucci on: printf("%s\n\n",nombre); N otese que ahora no se ponen corchetes, ya que queremos pasar a la funci on printf el vector de char nombre completo, no uno de sus elementos.
91
La funci on scanf() puede utilizarse para leer cadenas de caracteres desde teclado utilizando el formato %s, al igual que se hace con printf. Ejemplo: /******** Programa: Lectura.c Descripci on: Lee una cadena de caracteres con scanf Revisi on 0.0 ********/ #include <stdio.h> main(void) { char a[100]; scanf("%s",a); printf("Has escrito: %s\n",a); } Es importante tener en cuenta que el programa no puede saber cu antos caracteres va a escribir el usuario y por lo tanto la variable a debe tener un tama no sucientemente grande. En este ejemplo se ha jado el tama no del vector a en 100 elementos, por lo tanto el usuario puede escribir hasta 99 caracteres sin que falle el programa. Tambi en es importante tener en cuenta que la funci on scanf realiza un manejo un poco especial de los caracteres espacio (ASCII 32). La funci on scanf ignora todos los espacios que preceden a una palabra y deja de leer del teclado cuando encuentra un espacio; es decir, s olo vale para leer palabras sueltas. Una buena alternativa cuando se desea leer una frase completa es utilizar la funci on gets(). La funci on gets() permite leer desde teclado cadenas de caracteres que contengan espacios. Al leer una cadena de caracteres con gets todos los espacios que escriba el usuario se almacenan en la cadena. /******** Programa: Lectura2.c Descripci on: Lee una cadena de caracteres con gets Revisi on 0.0 ********/ #include <stdio.h> main(void) { char a[100]; gets(a); printf("Has escrito: %s\n",a); } Ejercicios 1. Escribir un programa que lea una cadena con scanf y con gets, comparando los resultados. Introducir una cantidad de caracteres mayor que la dimensi on de la cadena y ver
9.4. Matrices
La denici on y utilizaci on de matrices es an aloga al caso de los vectores, salvo que ahora tenemos dos dimensiones. Una matriz mat de tama no 3x5 para n umeros enteros se dene como: int mat[3][5]; El acceso a los 15 elementos de esta matriz se realiza siempre utilizando dos ndices que tambi en se escriben entre corchetes y que tambi en empiezan por cero. Por lo tanto los elementos de la matriz mat tiene los siguientes nombres: mat[0][0] mat[1][0] mat[2][0] mat[0][1] mat[1][1] mat[2][1] mat[0][2] mat[1][2] mat[2][2] mat[0][3] mat[1][3] mat[2][3] mat[0][4] mat[1][4] mat[2][4]
Al igual que en el caso de los vectores, no existen funciones especiales para imprimir ni operar con matrices por lo que generalmente se utilizan bucles for. En el caso de las matrices de dos dimensiones, como la del ejemplo anterior, se utilizan dos bucles for anidados para recorrer todos los elementos. El siguiente programa dene una matriz de 3x5 e inicializa a 7 todos sus elementos: /********************* Programa: matriz.c Descripci on: Inicializa a 7 todos los elementos de una matriz de enteros de 3x5 Revisi on 0.0: 14/ABR/1998 Autor: Rafael Palacios *********************/ main(void) { int mat[3][5]; int i,j; for(i=0; i<3; i++) { for(j=0; j<5; j++) mat[i][j]=7; } } }
93
El programa anterior quedar a de la siguiente manera al utilizar un par ametro N para el n umero de las y un par ametro M para el n umero de columnas. /********************* Programa: matriz2.c Descripci on: Inicializa a 7 todos los elementos de una matriz de enteros de N por M. Se utiliza #define para definir el valor de N y M. on 0.0: 14/ABR/1998 Revisi Autor: Rafael Palacios *********************/ #define N 3 #define M 5 main(void) { int mat[N][M]; int i,j; for(i=0; i<N; i++) { for(j=0; j<M; j++) mat[i][j]=7; } } } Si se quiere modicar el programa para que trabaje con matrices de tama no 30x50 basta con modicar las dos primeras l neas y volver a compilar. Todo el programa est a escrito en funci on de los dos par ametros, tanto el tama no de las matrices como los l mites de los bucles, y por lo tanto es v alido para cualquier valor de N y M. Ya se dijo en el cap tulo 3 que los nombres de variables suelen escribirse en min usculas, para mayor claridad al leer el programa. Para diferenciar las variables de los par ametros, e stos suelen escribirse en may usculas, como se ha hecho en el ejemplo. En caso de querer preguntar al usuario el tama no de las matrices con las que quiere calcular, habr a que denir tama nos jos sucientemente grandes (ya se ver a m as adelante c omo se denen matrices y vectores de tama no variable). Al utilizar tama nos jos es imprescindible comprobar, antes de iniciar los c alculos, que el valor introducido por el usuario no sea mayor que el tama no asignado para las matrices. El compilador no comprueba si se accede a elementos que se salen del vector por lo que el programa puede fallar estrepitosamente sin darnos muchas pistas. El siguiente ejemplo es un programa para multiplicar una matriz por un vector. /* Programa: Matriz1 Descripci on: Calcula el producto de una matriz por un vector Revisi on 0.1: mar/98 Autor: Rafael Palacios Hielscher */ #include <stdlib.h> #include <stdio.h>
main(void) { double mat[N][M]; /* Matriz */ dobule vec[M]; /* vector */ int i; /* contador */ int j; /* otro contador (necesario para recorrer la matriz)*/ int n; /* n umero de filas de la matriz */ int m; /* n umero de columnas de la matriz, que es igual al n umero de elementos del vector*/ double tmp; /* variable temporal */ printf("N umero de filas? "); scanf("%d",&n); printf("N umero de columnas? "); scanf("%d",&m); if (n>N || m>M) { printf("Error, este programa no vale para " "tama nos tan grandes.\n"); printf("Adi os\n"); exit(1); } /*** Lectura de datos ***/ /* matriz */ for(i=0; i<n; i++) { for(j=0; j<m; j++) { printf("mat[%d][%d]= ",i,j); scanf("%lf",&mat[i][j]); } } /* vector */ for(i=0; i<m; i++) { printf("vec[%d]= ",i); scanf("%lf",&vec[i]); } /*** C alculos y Salida ***/ for(i=0; i<n; i++) { tmp=0; for(j=0; j<m; j++) { tmp+=mat[i][j]*vec[j]; } printf("Resultado[%d]=%f\n",i,tmp); } }
9.6. EJERCICIOS
95
9.6. Ejercicios
1. Escribir un programa que pregunte el tama no de una matriz cuadrada, que pregunte todos sus elementos y que calcule la traspuesta. Luego debe mostrar el resultado ordenadamente (en forma de matriz). 2. Bas andose en el programa del producto de matriz por vector (programa Matriz1), escribir un programa para calcular el producto de dos matrices a y b. El resultado debe guardarse temporalmente en la matriz c y luego debe mostrarse por pantalla.
96
Cap tulo 10
Funciones
10.1. Introducci on
Una t ecnica muy empleada en la resoluci on de problemas es la conocida vulgarmente como divide y vencer as. Los programas de ordenador, salvo los m as triviales, son problemas cuya soluci on es muy compleja y a menudo dan aut enticos dolores de cabeza al programador. La manera m as elegante de construir un programa es dividir la tarea a realizar en otras tareas m as simples. Si estas tareas m as simples no son a un lo sucientemente sencillas, se vuelven a dividir, procediendo as hasta que cada tarea sea lo sucientemente simple como para resolverse con unas cuantas l neas de c odigo. A esta metodolog a de dise no se le conoce como dise no de arriba-abajo, mundialmente conocido como top-down por aquello del ingl es. Otras ventajas de la divisi on de un problema en m odulos claramente denidos es que facilita el trabajo en equipo y permite reutilizar m odulos creados con anterioridad si e stos se han dise nado de una manera generalista. Esta metodolog a se hace imprescindible hasta en los programas m as sencillos; por ello, todos los lenguajes de programaci on tienen soporte para realizar c omodamente esta divisi on en tareas simples. En el caso de C el mecanismo usado para dividir el programa en trozos son las funciones (que en otros lenguajes de programaci on como el FORTRAN o el BASIC, son llamadas subrutinas). Una funci on en C consta de unos argumentos de entrada, una salida, y un conjunto de instrucciones que denen su comportamiento. Esto permite aislar la funci on del resto del programa, ya que la funci on puede considerarse como un programa aparte que toma sus argumentos de entrada, realiza una serie de operaciones con ellos y genera una salida; todo ello sin interactuar con el resto del programa. Esta metodolog a de dise no presenta numerosas ventajas, entre las que cabe destacar: La complejidad de cada tarea es mucho menor que la de todo el programa, siendo abordable. Se puede repartir el trabajo entre varios programadores, encarg andose cada uno de ellos de una o varias funciones de las que se compone el programa. Esto permite cosas tales como reducir el tiempo total de programaci on (si tenemos un ej ercito de programadores), el que cada grupo de funciones las realice un especialista en el tema (por ejemplo las de c alculo intensivo las puede realizar un matem atico, las de contabilidad un contable. . . ). 97
98
99
NombreFunci on es el nombre de la funci on y ser a usado para llamar a la funci on desde cualquier parte del programa. Este nombre tiene las mismas limitaciones que los nombres de las variables discutidos en la secci on 3.2. La u nica diferencia es que generalmente los nombres compuestos suelen escribirse mezclando may usculas y min usculas, por ejemplo una funci on que borre la pantalla podr a llamarse BorrarPantalla() 1 . tipo 1 argumento 1,. . . ,tipo n argumento n es la lista de argumentos que recibe la funci on. Estos argumentos, tambi en llamados par ametros2, pueden ser cualquier tipo de dato de C: n umeros enteros o reales, caracteres, vectores. . . { es la llave que marca el comienzo del cuerpo de la funci on. instrucci on 1. . . instrucci on n son las instrucciones que componen el cuerpo de la funci on y se escriben con un sangrado de algunos espacios para delimitar m as claramente d onde comienza y d onde termina la funci on. return expresi on devuelta; es la u ltima instrucci on de la funci on y hace que e sta tera mine y devuelva el resultado de la evaluaci on de expresio n devuelta a quien la hab llamado. Si el tipo devuelto de la funci on es void, se termina la funci on con la instrucci on return; (sin expresi on devuelta). S olo en este caso se puede omitir la instrucci on return, con lo cual la funci on termina al llegar a la llave }. } es la llave que cierra el cuerpo de la funci on.
100
El uso de prototipos es opcional: el est andar de C s olo obliga a declarar las funciones si e stas devuelven un tipo distinto de int. Sin embargo el uso de prototipos es extremadamente recomendable pues permite al compilador detectar errores que, de no existir el prototipo, pasar an inadvertidamente al programa y e ste fallar a estrepitosamente al intentar ejecutarlo. En un intento de ahorrar al alumno innumerables dolores de cabeza en las llamadas a las funciones, en este curso cualquier funci on que aparezca sin su correspondiente prototipo implicar a una grave sanci on para el alumno (materializable en bajada de puntos, multas, tironcillos de orejas. . . ) La sintaxis del prototipo de una funci on es la siguiente: tipo devuelto NombreFunci on(tipo 1 argumento 1,. . . ,tipo n argumento n); Que como puede observarse se corresponde con la primera l nea de la denici on de la funci on pero terminada en un punto y coma, con lo que los m as avispados ya habr an adivinado que la t ecnica del cortar y pegar ser a sumamente u til en la escritura de prototipos.
10.4. Ejemplo
Para aclarar ideas veamos un ejemplo sencillo que ilustra el uso de una funci on dentro de un programa. Se necesita realizar un programa para calcular la suma de una serie de n umeros reales de la forma:
n a
a 1
La manera correcta de realizar la suma de la serie es denir una funci on para ello. Esto permitir a aislar esta tarea del resto del programa, poder usar la funci on en otros programas y todas las ventajas adicionales discutidas en la introducci on. La denici on de la funci on se realizar a de la siguiente manera: 37 /* Funci on: SumaSerie() 38 * 39 * Descripci on: Suma la serie aritm etica comprendida entre sus 40 * argumentos a y b. 41 * 42 * Argumentos: int a: Valor inicial de la serie. 43 * int b: Valor final de la serie. 44 * 45 * Valor devuelto: int: Resultado de la serie. 46 * 47 * Revisi on 0.1: 19/04/1998. 48 * 49 * Autor: El funcionario novato. 50 */ 51 52 int SumaSerie(int a, int b) 53 { 54 int suma; /* variable para almacenar la suma parcial de la serie*/ 55 56 suma = 0;
10.4. EJEMPLO
57 58 while(a<=b){ 59 suma +=a; 60 a++; 61 } 62 return suma; 63 }
101
En primer lugar cabe destacar la cha de la funci on. Es an aloga a la cha del programa pero a nadiendo la lista de argumentos y el valor devuelto. Esta cha es la parte m as importante de la funci on de cara a su reutilizaci on en otros programas, tanto por nosotros como por otros programadores, pues sin necesidad de leer ni una sola l nea de c odigo de la funci on podemos saber la tarea que realiza, los datos que necesitamos suministrarle y el valor que nos devuelve. Ni que decir tiene que en este curso la ausencia de la cha en una denici on de funci on implicar a castigos desmedidos contra el alumno. A continuaci on de la cha se encuentra la denici on de la funci on, en la que apreciamos en la primera l nea (52) el tipo devuelto, el nombre de la funci on y su lista de argumentos entre par entesis3 . En la siguiente l nea se encuentra la llave { que delimita el comienzo del cuerpo de la funci on. A continuaci on, ya dentro del cuerpo de la funci on, se encuentran las deniciones de las variables necesarias en el interior de la funci on. Estas variables se denominan variables locales puesto que s olo son accesibles desde dentro de la funci on. En este ejemplo la variable suma, declarada en la l nea 54, s olo es conocida dentro de la funci on y por tanto su valor s olo puede ser le do o escrito desde dentro de la funci on, pero no desde fuera 4. Despu es de las deniciones de las variables locales se encuentran las instrucciones necesarias para que la funci on realice su tarea. Cabe destacar que, tal como se aprecia en las l neas 58, 59 y 60, el uso de los par ametros de la funci on dentro del cuerpo de e sta se realiza de la misma manera que el de las dem as variables. on devolviendo Por u ltimo en la l nea 62 se usa la instrucci on return para salir de la funci el valor de suma. Veamos a continuaci on el resto del programa para ilustrar el modo de llamar a una funci on. 1 /* Programa: SumaSerie 2 * 3 * Descripci on: Calcula la suma de una serie aritm etica entre un valor 4 * inicial y un valor final. Para ello se apoya en la 5 * funci on SumaSerie(). 6 * 7 * Revisi on 0.1: 19/04/1998 8 * 9 * Autor: El funcionario novato. 10 */ 11 12 #include <stdio.h>
que no se termina esta sentencia con un punto y coma las variables locales se crean al entrar en la funci on y se destruyen al salir de ella, por lo que f sicamente es imposible conocer o modicar sus valores fuera de la funci on porque las variables ni siquiera existen.
4 De hecho 3 N otese
102 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <stdlib.h> /* prototipos de las funciones */ int SumaSerie(int a, int b);
main(void) { int inicial; /* Valor inicial de la serie */ int final; /* Valor final de la serie */ int resultado; /* Resultado de la suma de la serie */ printf("Valor inicial de la serie: "); scanf("%d", &inicial); printf("Valor final de la serie: "); scanf("%d", &final); resultado = SumaSerie(inicial, final); printf("El resultado es: %d\n", resultado); }
En donde lo primero que cabe destacar es la inclusi on del prototipo de la funci on al principio del chero (l nea 17). Esto permite que el compilador conozca el prototipo antes de que ocurra cualquier llamada a la funci on SumaSerie() dentro del chero y pueda comprobar que las llamadas a dicha funci on son correctas (n umero de argumentos, tipos y valor devuelto). Dentro de la funci on main() se piden al usuario los valores inicial y nal de la serie y en la l nea 31 se llama a la funci on SumaSerie(). Esta l nea ilustra dos aspectos muy importantes de las funciones en C: Una funci on que devuelve un tipo t se puede usar en cualquier expresi on en donde se puede usar una variable del mismo tipo t. En nuestro caso se puede introducir en cualquier lugar en el que introducir amos un n umero o una variable de tipo int, como por ejemplo en una asignaci on como ha sido el caso. Los nombres de los argumentos en la llamada no tienen por qu e ser iguales a los nombres usados para dichos argumentos en la denici on de la funci on. Esta caracter stica, junto con la posibilidad de trabajar con variables locales, es la que permite reutilizar f acilmente las funciones.
103
al salir de e sta. En la llamada a la funci on (l nea 31) los valores almacenados en las variables inicial y final se copian en las variables temporales a y b con lo que cualquier operaci on que se realice con estas copias (lecturas, escrituras, incrementos. . . ) no inuir an para nada en las variables originales. Ejercicios 1. Para ilustrar lo que acabamos de discutir, introducir el programa anterior en el ordenador y modicarlo para que se imprima en la pantalla el valor inicial de la serie, el valor nal y su suma. En primer lugar realizar la impresi on desde main() y en segundo lugar desde dentro de la funci on SumaSerie() observando las diferencias. Esto que se acaba de discutir es uno de los aspectos m as importantes de las funciones: el hecho de que la funci on trabaje con sus propias variables la aisla del resto del programa, pues se evita modicar de forma accidental una variable que no pertenezca a la funci on. Para ilustrar este aspecto, imaginemos que el programa en el que hay que realizar la suma de la serie descrita en el apartado anterior es mucho m as largo y que como no hacemos nunca caso del profesor, decidimos no usar funciones en el programa. Para sumar la serie se podr a hacer: ... suma = 0; while(inicial<=final){ suma +=inicial; inicial++; } ... Los problemas que plantea esta soluci on son varios: En primer lugar el valor inicial nos lo hemos cargado al ir increment andolo, por lo que si intentamos realizar la tarea propuesta en el ejercicio 1 nos daremos cuenta que el valor inicial es igual que el nal despu es de sumarse la serie. Hemos usado una variable llamada suma para almacenar la suma parcial de la serie, pero que ocurre si en otra parte del programa se ha usado una variable tambi en llamada suma? Obviamente el valor que tuviese desaparece con el c alculo de la serie, y si se usa despu es el programa fallar a estrepitosamente y para colmo ser a dif cil encontrar el error. Si en otra parte se quiere calcular otra serie entre las variables ini y fin y queremos aprovechar este trozo de c odigo tendremos que cortar y pegar esta parte del programa en la parte apropiada y luego cambiar inicial por ini y final por fin y tal vez suma por cualquier otra cosa si queremos conservar el valor de la serie anterior. Adem as, si se nos olvida cambiar alguna de las variables, el compilador no dar a ning un mensaje de error ya que todas las variables son v alidas; pero sin embargo el resultado ser a incorrecto. Como puede comprobar el alumno, las ventajas de usar funciones son muy numerosas y el u nico inconveniente es aprender a usarlas, pero como esto va a ser imprescindible para aprobar el curso, no s e a que est as esperando para empezar a practicar.
104
105
10.8. Ejemplos
Veamos a continuaci on algunos ejemplos m as para aclarar ideas.
106 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
/* Funci on: cuadrado() * on: Devuelve el cuadrado de su argumento * Descripci * * Argumentos: double a: Valor del que se calcula el cuadrado. * * Valor devuelto: double: El argumento a al cuadrado. * on 0.0: 15/04/1998. * Revisi * * Autor: El funcionario novato. */ double cuadrado(double a) { return a*a; }
N otese que la llamada a la funci on cuadrado() aparece ahora dentro de la lista de argumentos de printf() (l nea 22). Tal como se dijo en la secci on 10.4 una llamada a funci on puede aparecer en cualquier lugar donde lo har a un objeto del mismo tipo que el devuelto por la funci on. Como la funci on cuadrado es de tipo double puede aparecer en cualquier lugar en el que pondr amos un double y ciertamente un argumento de printf() es un buen lugar para un double. Ejercicios 1. Modicar el programa anterior para calcular el cuadrado de un n umero entero. Modicar en primer lugar s olo el programa principal, de forma que se lea un n umero entero y se llame a la misma funci on de antes y se imprima el resultado. Funciona todo correctamente? Que conversiones autom aticas se est an realizando? 2. Modicar ahora la funci on cuadrado para que calcule el cuadrado de un int e integrarla en el programa anterior. 3. Usar la funci on cuadrado() dise nada en el ejercicio 2 en el ejemplo de los apuntes. Funciona correctamente? Que tipo de conversiones se realizan?
10.8.2. Factorial
Una funci on muy usada en estad stica que no esta disponible en la librer a matem atica est andar de C es el factorial. En este ejemplo vamos a realizar una funci on que calcula el factorial de un n umero entero. La funci on junto con un programa que la usa se muestra a continuaci on: 1 /* Programa: Factoriales 2 * 3 * Descripci on: Ejemplo de uso de la funci on factorial(). 4 * 5 * Revisi on 0.0: 15/04/1998
10.8. EJEMPLOS
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 * * Autor: El funcionario novato. */ #include <stdio.h> /* prototipos de las funciones */ long int factorial(long int a); main(void) { long int valor; printf("Introduzca un valor "); scanf("%ld",&valor); printf("El factorial de %ld es: %ld\n", valor, factorial(valor)); } /* Funci on: factorial() * * Descripci on: Devuelve el factorial de su argumento * * Argumentos: long int a: Valor del que se calcula el factorial. * * Valor devuelto: long int: El factorial del argumento a. * on 0.0: 15/04/1998. * Revisi * * Autor: El funcionario novato. */ long int factorial(long int a) { long int fact; /* valores parciales de factorial */ fact = 1; while(a>0){ fact *= a; a--; } return fact; }
107
Ejercicios 1. Introducir el programa anterior en el ordenador y comprobar su funcionamiento para valores positivos y negativos. Da siempre resultados correctos? 2. Modicar la funci on factorial() para que devuelva -1 en caso de que no se pueda
108
109
/* Funci on: ProdEscalar() * on: Devuelve el producto escalar de dos vectores de 3 dimensiones * Descripci * * Argumentos: double vect1[]: primer vector. * double vect2[]: segundo vector. * * Valor devuelto: double: El producto escalar de los dos vectores * on 0.0: 15/04/1998. * Revisi * * Autor: El funcionario novato. */ double ProdEscalar(double vect1[], double vect2[]) { int i; double producto; producto = 0; for(i=0; i<3; i++){ producto += vect1[i]*vect2[i]; } return producto; }
En este ejemplo se han introducido dos novedades: La primera es la inicializaci on de los vectores realizada en las l neas 18 y 19. Este tipo de inicializaci on de los vectores asigna al primer elemento del vector el primer elemento de la lista entre llaves, al segundo elemento el segundo valor y as sucesivamente hasta terminar la lista. As pues la l nea 18 es equivalente a las l neas: vect1[0] = 1; vect1[2] = 2; vect1[3] = 3; pero mucho m as compacta como habr a observado el lector. La segunda novedad es la manera de llamar a una funci on pas andole un vector. Esto se ha mostrado en la l nea 21, en la que se llama a la funci on ProdEscalar(). Como puede observar para pasar los vectores a la funci on se escribe su nombre sin corchetes en la lista de argumentos: ProdEscalar(vect1, vect2). Por u ltimo destacar que, aunque en el prototipo y en la denici on de la funci on no se especica el tama no de los vectores, dentro del cuerpo de la funci on se supone que son de dimensi on tres, pues el bucle que realiza el producto escalar comienza en la coordenada 0 y termina en la 2, tal como se aprecia en las l neas 44 a 46. Esto es un grave inconveniente, pues no hay manera de comprobar desde dentro de la funci on la dimensi on de los vectores que se han pasado como argumentos. Por tanto es necesario ser muy cuidadoso cuando se pasen vectores o matrices a funciones y pasar siempre los vectores de la longitud adecuada. Si se desea realizar una funci on que trabaje con vectores de cualquier dimensi on habr a que indicar de alguna manera el tama no del vector, por ejemplo mediante un argumento adicional.
110
As , si se generaliza la funci on ProdEscalar() para trabajar con vectores de cualquier dimensi on, el programa anterior queda:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 /* Programa: Producto Escalar * * Descripci on: Ejemplo de uso de la funci on ProdEscalar(). * * Revisi on 0.1: 20/04/1998 * * Autor: El funcionario novato. */ #include <stdio.h> /* prototipos de las funciones */ double ProdEscalar(int dimension, double vect1[], double vect2[]); main(void) { double vect1[3]={1, 2, 3}; double vect2[3]={3, 2, 1}; printf("El producto escalar vale %g\n", ProdEscalar(3, vect1, vect2)); } /* Funci on: ProdEscalar() * on: Devuelve el producto escalar de dos vectores cuya * Descripci on viene dada por el primer argumento * dimensi * on de los vectores on: Dimensi * Argumentos: int dimensi * double vect1[]: primer vector. * double vect2[]: segundo vector. * * Valor devuelto: double: El producto escalar de los dos vectores * on 1.0: 20/04/1998. * Revisi * * Autor: El funcionario novato. */ double ProdEscalar(int dimension, double vect1[], double vect2[]) { int i; double producto; producto = 0; for(i=0; i<dimension; i++){ producto += vect1[i]*vect2[i]; } return producto; }
111
3. El prototipo
112 double Det(double mat[][3]); 2. Escribir una funci on para multiplicar dos matrices de 3
3. El prototipo ser a:
Cap tulo 11
Punteros
11.1. Introducci on
Los punteros son un tipo de variable un poco especial, ya que en lugar de almacenar valores (como las variables de tipo int o de tipo double) los punteros almacenan direcciones de memoria. Utilizando variables normales s olo pueden modicarse valores, mientras que utilizando punteros pueden manipularse direcciones de memoria o valores. Pero no se trata simplemente de un nuevo tipo de dato que se utiliza ocasionalmente, sino que los punteros son parte esencial del lenguaje C. Entre otras ventajas, los punteros permiten: Denir vectores y matrices de tama no variable, que utilizan s olo la cantidad de memoria necesaria y cuyo tama no puede reajustarse din amicamente. Denir estructuras de datos m as completas como listas de datos o a rboles. Que las funciones devuelvan m as de un valor.
114
Direccin Valor Variable
C2B8
pti
C2B8
123
C37A
ptd
C37A
7.4e-3
Figura 11.1:
de memoria contenida en ptd es un double. Se dice por lo tanto que pti apunta a un entero (puntero a entero) mientras que ptd apunta a un double (puntero a double). En la gura 11.1 aparece un ejemplo en el que pti apunta a una direcci on de memoria (0xC2B8) donde se encuentra almacenado el valor entero 123, y ptd apunta a otra direcci on de memoria (0xC37A) donde se encuentra almacenado el valor 7.4e-3.
ser a una casualidad que en la direcci on de memoria 0xC2B8 hubiese un valor entero. Por lo tanto en los pr oximos ejemplos vamos a hacer que nuestros punteros apunten a datos correctos mediante la declaraci on de variables auxiliares. Esto es algo que no parece muy u til, pero vale para explicar el funcionamiento de los punteros. El operador & se utiliza para obtener la direcci on de memoria de cualquier variable del programa. Si nuestro programa tiene una variable entera que se llama ii, entonces podemos obtener la direcci on de memoria que el compilador ha preparado para almacenar su valor escribiendo &ii. int ii; int *pti; ii=78; pti=ⅈ /* variable entera */ /* puntero a entero */ /* inicializo ii */ /* pti apunta a ii */
Al aplicar el operador & a la variable ii no se obtiene el valor entero almacenado en ella sino la direcci on de memoria en donde se encuentra dicho valor. Por lo tanto &ii es un puntero a entero porque es una direcci on de memoria donde se haya un entero. Suponiendo que el compilador reserva la posici on de memoria 0x34FF para la variable ii, la ejecuci on del programa anterior llevar a a la organizaci on de memoria que se muestra en la gura 11.2
115
34FF
pti
34FF
78
ii
Figura 11.2:
El compilador dar a un aviso si se intenta realizar una asignaci on en la que no corresponden los tipos, por ejemplo al asignar &ii a un puntero a double, ya que &ii devuelve un puntero a int. Este tipo de comprobaciones del compilador evita muchos errores de programaci on. Por tanto hay que estar muy atento a los mensajes del compilador y hay que activar todos los avisos. Asignaciones entre dos punteros tambi en son v alidas siempre que los dos punteros sean del mismo tipo. Por ejemplo, si se compila el programa siguiente: 1 2 3 4 5 6 7 8 9 10 11 12 13 #include <stdio.h> main() { int ii; double *pd; ii = 27; pd = ⅈ /* ERROR */ printf("%lf\n", *pd*2); }
el compilador (gcc en este caso1 ) generar a el siguiente aviso: tema11_2.c:10: warning: assignment from incompatible pointer type donde se avisa que en la l nea 10 se asigna al puntero pd la direcci on de un dato que no es compatible con su tipo. A pesar de este aviso se genera un programa que si se ejecuta imprime por pantalla el valor -3.997604 en lugar de 54. Ejercicios 1. Sabr a explicar por qu e el programa anterior imprime un valor err oneo?
116
Situacin inicial Direccin Valor Variable Despus de hacer *pti+=8; Direccin Valor Variable
34FF
pti
34FF
pti
351F
pti
34FF
78
ii
34FF
86
ii
34FF
86
ii
351F
????
Figura 11.3:
n, permite acceder al valor de memoria. El operador unario *, llamado operador de indireccio por medio del puntero. int ii; int *pti; pti=ⅈ *pti=78; /* variable entera */ /* puntero a entero */ /* pti apunta a ii */ /* equivale a hacer ii=78; */
La asignaci on *pti=78 introduce el valor entero 78 en la posici on de memoria almacenada en el puntero pti. Como previamente se ha almacenado en pti la direcci on de memoria de la variable ii, la asignaci on equivale a dar el valor 78 directamente a la variable ii. Es decir, tras la asignaci on pti=&ii disponemos de dos maneras de manipular los valores enteros almacenados en la variable ii: directamente mediante ii o indirectamente mediante el puntero pti. De momento esto no parece muy u til pero es importante que quede claro.
117
De nuevo hacer que el puntero pase a apuntar a un lugar desconocido vuelve a ser peligroso y habr a que tener cuidado con este tipo de operaciones. Es muy importante distinguir entre las operaciones con punteros utilizando el operador * y sin utilizarlo. Utilizando el operador * el puntero se convierte en una variable normal y por lo tanto admite todas las operaciones de estas variables. Por ejemplo ii=20 * *pti, *pti=a+b, *pti+=7 etc. En operaciones normales de suma, resta, multiplicaci on, divisi on, comparaciones... no hace falta ning un cuidado especial, s olo que el asterisco vaya unido al nombre del puntero para que no se confunda con el operador de multiplicaci on. Las operaciones de incremento y decremento puede dar lugar a confusi on, por la mezcla de operadores, y se aconseja escribirlas siempre con par entesis adicionales: ++(*p), (*p)++, --(*p) y (*p)--. on del puntero y por lo tanto cambia el Sin utilizar el operador * se manipula la direcci lugar al que apunta. La operaci on m as normal es incrementar la direcci on en uno, con ello se pasa a apuntar al dato que ocupa la siguiente posici on de memoria. La aplicaci on fundamental de esta operaci on es recorrer posiciones de memoria consecutivas. Adem as de valer para sgonear la memoria del ordenador, e sto se utiliza para recorrer vectores (cuyos datos siempre se almacenan en posiciones de memoria consecutivas). Pero antes de pasar al apartado de vectores veamos cuanto se incrementa realmente un puntero. Generalmente cada posici on de memoria del ordenador puede almacenar un byte, por lo tanto aumentando la direcci on de un puntero en 1 pasar amos a apuntar al siguiente byte de la memoria. Esto es algo bastante claro aunque dif cil de manejar, porque como se ha visto repetidamente a lo largo del curso, cada tipo de variable ocupa un tama no de memoria diferente. Por ejemplo los char s ocupan 1 byte, pero los int ocupan 2 o 4 bytes, los double ocupan 8, etc. Adem as, estos tama nos dependen del ordenador y del compilador que se utilice. Para solucionar este problema, los punteros de cualquier tipo siempre apuntan al primer byte de la codicaci on de cada dato. Cuando se accede al dato mediante el operador *, el compilador se encarga de acceder a los bytes necesarios a partir de la direcci on almacenada en el puntero. En el caso de punteros a char s olo se accede al byte almacenado en la direcci on de memoria del puntero, mientras que el caso de punteros a double se accede a la posici on de memoria indicada en el puntero y a los 7 bytes siguientes. Todo este mecanismo ocurre de manera autom atica y el programador s olo debe preocuparse de declarar punteros a char cuando quiere trabajar con valores char o punteros a double cuando quiere trabajar con valores double. Tambi en para facilitar las cosas, la operaci on de sumar 1 a un puntero hace que su direcci on se incremente la cantidad necesaria para pasar a apuntar al siguiente dato del mismo tipo. Es decir s olo en el caso de variables que ocupan 1 byte en memoria (variables char) la operaci on de incremento aumenta en 1 la direcci on de memoria, en los dem as casos la aumenta m as. Lo que hay que recordar es que siempre se incrementa un dato completo y no hace falta recordar cu anto ocupa cada dato en la memoria. Por ejemplo, si ptc es un puntero a char que vale 0x3B20, la operaci on ptc++ har a que pase a valer 0x3B21. Por otro lado si ptd es un puntero a double que vale 0x3B20, la operaci on ptd++ har a que pase a valer 0x3B28 (ver la gura 11.4).
118
pti++ Direccin Valor Variable
Direccin
Valor
Variable
3B20
pti
3B20 3B21
1 byte 1 byte
3B20
ptd
Figura 11.4:
puntero. /* Programa de punteros y vectores Descripci on: Este programa inicializa un vector y luego escribe sus valores Versi on: 1.0 */ #define N 10 #include <stdio.h> main(void) { double a[N]; int i; double *pd;
/*** Inicializaci on ***/ for(i=0; i<N; i++) { a[i]=7.8*i; } /*** Imprimir valores ***/ pd=&a[0]; /* Apunto al primer elemento del vector */
119
Hay que tener en cuenta que cuando termina el programa el puntero pd queda apuntado a un lugar no v alido, ya que queda fuera del vector a. Supongamos ahora que queremos copiar el vector a en el vector b de manera que sea su inverso; es decir, si a vale [1,2,3,4] queremos que b valga [4,3,2,1]. Generar b a partir de a es muy sencillo utilizando dos punteros pa y pb, uno creciente y otro decreciente: pa=&a[0]; /* pa apunta al primer elemento del vector a */ pb=&b[N-1]; /* pb apunta al ultimo elemento del vector b */ for(i=0; i<N; i++) { *pb=*pa; /* copio un elemento */ pa++; /* avanzo un elemento */ pb--; /* retrocedo un elemento */ } Con mucha frecuencia se utilizan varios punteros sobre el mismo vector para copiar datos o hacer manipulaciones sin tener que andar con la complicaci on de manejar varios ndices. Los ejemplos m as t picos son las transformaciones de cadenas de caracteres. Ejercicios 1. Escribir un programa que obtenga la simetr a de una cadena de caracteres. Por ejemplo, on" el resultado debe ser "avi onn oiva". si la cadena vale "avi
programa no puede cambiar la direcci on almacenada en un puntero constante. Por ejemplo a=&ii es ilegal
120 *(a+3)=2.7;
Adem as este operador se puede aplicar sobre cualquier puntero, no s olo sobre los punteros constantes, por lo que los dos trozos de c odigo que siguen son equivalentes: a[3]=2.7; pd=a; pd[3]=2.7;
A modo de resumen, si tenemos un vector vec de 100 elementos y queremos asignar el valor 45 al 5o elemento, pondremos: vec[4]=45; En caso de tener un puntero punt que apunta al primer elemento de vector, podemos realizar la misma asignaci on de tres maneras: Moviendo el puntero y luego volviendo atr as punt+=4; *punt=45; punt-=4; Calculando la direcci on directamente *(punt+4)=45; Utilizando corchetes, que es lo m as c omodo. punt[4]=45;
121
Esta funci on no hace nada porque cambia los valores de a y b, pero e stos son copias de las variables que se utilizan al llamar a la funci on. Si la llamada es cambio(x,y) entonces a es una copia de x y b es una copia de y. Aunque la funci on cambia los valores de a y b, al terminar y volver al programa principal, los valores de x e y no se ven alterados. La manera de conseguir que la funci on cambio sea u til es trabajar con punteros. La versi on 1.1 de esta funci on utiliza como argumentos dos punteros en lugar de dos enteros; el nuevo prototipo es: void cambio(int *pa, int *pb); Al recibir las direcciones de las dos variables, s tenemos acceso a los valores originales 3 y por lo tanto se puede hacer el cambio. Ahora se trata de cambiar los valores contenidos en las direcciones apuntadas por los punteros pa y pb. /* Funci on: Cambio() Descripci on: Intenta cambiar el valor de dos variables Comentario: OK on: 1.1 Revisi Autor: El que sabe de punteros */ void cambio(int *pa, int *pb) { int tmp; tmp=*pa; *pa=*pb; *pb=tmp; } La u nica duda ser a C omo llamamos a la funci on para darle las direcciones en lugar de los valores? La respuesta ya se ha visto porque el operador & nos facilita la direcci on de cualquier variable. En este caso la llamada para cambiar los valores de las variables x e y es la siguiente: cambio(&x, &y); De hecho, llevamos utilizando llamadas de este estilo desde que se mencion o por primera vez la funci on scanf. Por qu e en la funci on scanf se escriben las variables con un & mientras que en printf se escriben sin nada? La respuesta es sencilla despu es de haber entendido los punteros y las funciones. La funci on printf se encarga de escribir el valor de nuestras variables y le da igual tener acceso directo al valor o trabajar sobre una copia; para mayor facilidad de programaci on (y para mayor seguridad) se opta por pasarle una copia. Sin embargo la funci on scanf lee un valor del teclado y lo escribe dentro de una de las variables de la funci on que la llama. Por lo tanto necesita acceso directo a la direcci on de memoria donde se almacena el valor de la variable. Y la u ltima pregunta es Por qu e es tan importante especicar correctamente el formato de las variables en scanf? Porque si el formato dice %f la funci on scanf escribe en total 4 bytes (sizeof(float)) a partir de la direcci on de memoria, pero si el formato dice %lf entonces scanf escribe 8 bytes. Cuando la variable no coincide con el tipo especicado pueden ocurrir dos cosas: que se escribe fuera de las posiciones de memoria reservadas para la variable (probablemente machacando la variable de al lado) o que no se escribe suciente (dejando parte de la variable sin inicializar).
3 Usando
122
123
se termina de usar la memoria, esta debe liberarse al sistema operativo para que los dem as programas puedan usarla. Esta t ecnica permite por tanto un manejo m as racional de la memoria, aparte de permitirnos crear estructuras de datos m as complejas como listas enlazadas y a rboles. Sin embargo estos temas quedan fuera del alcance de este curso.
124
devuelve un puntero de tipo void. Este tipo es un puntero gen erico que ha de convertirse mediante un molde (cast) al tipo de puntero con el que vamos a manejar el bloque. En el ejemplo, como se deseaba crear un vector de 100 enteros, el puntero devuelto se ha convertido en un puntero a entero mediante el molde (int *). Por u ltimo conviene destacar que en las llamadas a las funciones calloc() y malloc() se ha usado sizeof(int) para especicar la cantidad de memoria que se necesita. Esto es muy importante de cara a la portabilidad del programa. Si aprovechando que conocemos el tama no de un int en un PC con GCC hubi esemos puesto un 4 en lugar de sizeof(int), el programa no funcionar a bien si lo compilamos en Borland C para MS-DOS, el cual usa enteros de 2 bytes.
que el sistema operativo sea muy astuto. liberarla varias veces, que es el error m as habitual.
125
Tambi en es un grave error el seguir usando la memoria una vez liberada, pues aunque tenemos un puntero que apunta al principio del bloque, este bloque ya no pertenece a nuestro programa y por tanto puede estar siendo usado por otro programa (o por nuestro programa si se ha solicitado m as memoria), por lo que cualquier lectura nos puede devolver un valor distinto del escrito y cualquier escritura puede modicar datos de otro programa (o de otro bloque del nuestro), tal como se muestra en el siguiente ejemplo: int *pent; /* puntero al principio de la vector */ int a; /* Variable auxiliar */ pent = (int *) calloc(100, sizeof(int)); if(pent == NULL){ /* Si no hay memoria */ printf("Error: No hay suficiente memoria\n"); exit(1); } pent[0] = 27; /* recu erdese la equivalencia entre punteros y vectores */ free(pent); a = pent[0]; /* ERROR! El bloque al que apunta pent ya no pertenece a nuestro programa. Por tanto puede que en a no se escriba 27 */ pent[0] = 40; /* ERROR! Estamos modificando memoria que ya no es nuestra y los resultados pueden ser catastr oficos */
11.6.3. Ejemplo
Para jar ideas vamos a ver a continuaci on un programa en el que se hace uso de asignaci on din amica de memoria. Se ha de escribir un programa que pida un vector al usuario y una vez que e ste lo haya introducido, ha de generar otro vector que contenga s olo los elementos del primer vector que sean n umeros pares e imprimirlos en la pantalla. Por supuesto la dimensi on de ambos vectores es desconocida antes de ejecutar el programa. Como no se conoce la dimensi on de ninguna de los vectores y sabemos ya un mont on acerca de asignaci on din amica de memoria decidimos, en lugar de crear dos vectores enormes que valgan para cualquier caso, crear los vectores del tama no justo cuando se ejecute el programa. Un pseudoc odigo del programa se muestra a continuaci on: on del vector al usuario Pedir dimensi Pedir memoria para el vector Pedir al usuario cada uno de los elementos del vector umero de elementos pares del vector Contar el n umeros pares. Pedir memoria para el vector de n umeros pares del primer vector al segundo Copiar los n Imprimir el segundo vector El programa completo se muestra a continuaci on:
1 /* Programa: BuscaPar 2 *
126
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56
#include <stdio.h> #include <stdlib.h> main(void) { int *pvec_usu; /* Puntero al vector introducido por el usuario (creado din amicamente) */ int *pvec_par; /* Puntero al vector de elementos pares (creado din amicamente)*/ int dim_usu; /* Dimensi on del vector del usuario */ int dim_par; /* Dimensi on del vector de elementos pares */ int n; /* Indice para los for */ int m; /* Indice para recorrer la matriz de elementos pares */ printf("Introduzca la dimensi on del vector: "); scanf("%d",&dim_usu); /* Asignamos memoria para el vector del usuario. Contiene dim_usu enteros */ pvec_usu = (int *) calloc(dim_usu, sizeof(int)); if(pvec_usu == NULL){ /* Estamos sin memoria */ printf("Error: no hay suficiente memoria para un vector de %d elementos\n", dim_usu); exit(1); } /* Pedimos los elementos del vector */ for(n=0; n<dim_usu; n++){ printf("Elemento %d = ", n); scanf("%d", &(pvec_usu[n]) ); } /* Contamos los pares en la variable dim_par */ dim_par = 0; /* De momento no hay ning un elemento par */ for(n=0; n<dim_usu; n++){ if( (pvec_usu[n]%2) == 0 ){ /* es par */ dim_par ++; } } /* Se asigna memoria para los n umeros pares */ pvec_par = (int *) calloc(dim_par, sizeof(int));
127
if(pvec_par == NULL){ /* Estamos sin memoria */ printf("Error: no hay suficiente memoria para un vector de %d elementos\n", dim_par); free(pvec_usu); /* Antes de salir hay que LIBERAR la memoria que ya nos hab an dado */ exit(1); } /* Se copian los elementos pares */ m = 0; /* Indice para el vector de elementos pares (inicialmente apunta al primer elemento) */ for(n=0; n<dim_usu; n++){ if( (pvec_usu[n]%2) == 0 ){ /* es par */ pvec_par[m] = pvec_usu[n]; /* copio el elemento */ m++; /* e incremento el ndice del vector par */ } } /* Se imprimen el vector de elementos pares */ printf("\n-----------------------\n"); /* Para separar los dos vectores */ for(n=0; n<dim_par; n++){ printf("Elemento par %d = %d\n", n, pvec_par[n]); } /* y antes de salir se libera la memoria que nos hab an asignado */ free(pvec_usu); free(pvec_par); exit(0);
Un ejemplo de la ejecuci on del programa se muestra a continuaci on: Introduzca Elemento 0 Elemento 1 Elemento 2 la dimensi on del vector: 3 = 1 = 2 = 4
----------------------Elemento par 0 = 2 Elemento par 1 = 4 Ejercicios 1. Modicar el programa anterior para que la petici on de datos se realice en una funci on. El prototipo de dicha funci on ha de ser: void PideVec(int *pvec, int dimension) 2. Puesto que los vectores siempre se recorren desde principio a n y de elemento en elemento puede ser m as f acil usar punteros para recorrer los vectores. Modicar el programa del apartado anterior para que la b usqueda de elementos pares, la creaci on de la matriz de n umeros pares y su impresi on, se realicen mediante punteros en lugar de
128
Cap tulo 12
Archivos
12.1. Introducci on
Cualquiera que haya manejado un ordenador habr a experimentado con desolaci on como un corte de corriente hace que desaparezca en un momento todo el trabajo que no hab a guardado en el disco. La raz on de semejante desastre estriba en que la memoria RAM del ordenador se borra en cuanto se corta la corriente. Adem as la memoria RAM es muy cara por lo que su cantidad es limitada, pero sin embargo es necesario que dentro de un ordenador se almacenen una gran cantidad de programas y de datos que pueden ocupar varios Gigabytes. Por todo esto es necesario un sistema de almacenamiento secundario que no se borre al cortar la corriente y que tenga una gran capacidad. El sistema actual de almacenamiento secundario m as usado es el disco duro, dada su rapidez y capacidad de almacenamiento, aunque existen una gran variedad de sistemas como diskettes, CDROM. . . El manejo de todos estos sistemas lo realiza el sistema operativo, el cual organiza la informaci on en forma de archivos repartidos en directorios (tambi en llamados carpetas). De esta forma cada archivo tiene una identicaci on u nica dentro del ordenador compuesta por el nombre del archivo y por la ruta o camino de acceso hasta e l por el a rbol de directorios. Hasta ahora los programas que hemos realizado en este curso han tomado datos desde el teclado y han impreso sus resultados en la pantalla. Sin embargo en muchas ocasiones es necesario leer datos desde un archivo que se ha almacenado previamente en el disco (bien por el propio usuario o bien por otro programa), escribir los resultados en un archivo de forma que pueda ser usado posteriormente tanto por el usuario como por otro programa o usar un archivo en disco como medio de almacenamiento de los datos de un programa de forma que se mantengan cuando se apague el ordenador (por ejemplo en aplicaciones de bases de datos). La biblioteca est andar de C proporciona un conjunto de funciones para el manejo de archivos. En este tema introductorio s olo se van a estudiar cuatro funciones b asicas que permiten trabajar con archivos de texto.
130 Modo r r+ w w+ a
FILE *fopen(char *Nombre completo del archivo, char *modo); En donde Nombre completo del archivo es una cadena de caracteres que contiene el nombre del archivo, incluyendo el camino completo si dicho archivo no esta situado en el directorio actual. Este nombre ha de ser adem as un nombre legal para el sistema operativo. Por ejemplo si se trabaja en MS-DOS el nombre de archivo no puede tener m as de ocho caracteres seguidos de un punto y tres caracteres m as para la extensi on. El modo del archivo es otra cadena de caracteres que indica el tipo de operaciones que vamos a realizar con e l. En la tabla 12.1 se muestran todos los modos aceptados por la funci on. El valor devuelto por la funci on es un puntero a archivo, que es un puntero que apunta a una estructura de datos llamada FILE que est a denida en stdio.h y que contiene toda la informaci on que necesitan el resto de las funciones que trabajan con archivos, como el modo del archivo, los buffers del archivo, errores. . . Si ocurre alg un tipo de error en la apertura (como intentar abrir un archivo que no existe en modo "r") fopen() devuelve un NULL. Por tanto despu es de la llamada a fopen() hay que vericar que el puntero devuelto es v alido (al igual que se realiza con los punteros devueltos por calloc() y malloc()). Por ejemplo si queremos abrir un archivo para leer de e l cuyo nombre es pepe habr a que escribir: #include <stdio.h> ... main(void) { FILE *pfich; ... pfich = fopen("pepe", "r+"); if(pfich == NULL){ printf("Error: No se puede abrir el archivo.\n"); exit(1); } ...
1 En
131
en donde se ha supuesto que el archivo pepe est a situado en el directorio de trabajo actual. En caso de que no exista el archivo pepe o si no se encuentra en el directorio actual, fopen() no podr a abrirlo y devolver a un NULL, por lo que el programa despu es de imprimir un mensaje de error terminar a su ejecuci on. Si se quiere abrir un archivo para a nadir al nal de e l m as datos y dicho archivo est a situado en el directorio c:\dani con el nombre datos.m, el programa ser a igual que el anterior salvo que la llamada a fopen() ser a: pfich = fopen("c:\\dani\\datos.m", "a"); Recu erdese que \\ es traducido por el compilador de C por una \ al generar la cadena de caracteres, de la misma manera que \n se traduce por un salto de carro. El archivo abierto ser a por tanto c:\dani\datos.m.
132
if( fclose(pfich) != 0){ printf("Error al cerrar el archivo\n"); printf("Es posible que se haya producido una p erdida de datos.\n"); printf("Lo siento.\n"); }
/* /* /* /*
Valor devuelto por scanf */ Puntero al archivo */ dato le do del fichero */ Suma de los datos */
pfich = fopen("pepe", "r+"); if(pfich == NULL){ printf("Error: No se puede abrir el fichero \"pepe\"\n"); exit(1); } sum_tot = 0; /* Inicializaci on de la suma antes de entrar en el bucle*/ n = fscanf(pfich, "%lf", &dato); while(n!=EOF && n!=0){ /* termina en cuanto ocurra un error de lectura*/ sum_tot+=dato; n = fscanf(pfich, "%lf", &dato); } printf("El valor de la suma es: %lf\n", sum_tot); if( fclose(pfich) != 0){ printf("Error al cerrar el fichero\n"); exit(1); } }
134 123.4 al ejecutarse imprimir a por pantalla: El valor de la suma es: 132.600000
Como se puede apreciar en las l neas 27 y 30, el uso de fscanf() es id entico al uso de scanf() salvo por la existencia del puntero al archivo desde donde se lee (pfich). Tambi en cabe destacar la condici on de salida del bucle while. Como se puede apreciar en la l nea 28 se sale cuando el valor devuelto por fscanf() sea EOF, en cuyo caso se habr a acabado el chero y no tiene sentido seguir leyendo; o cuando devuelva un cero, indicando que no se ha le do el dato debido a un error de formato. Seg un esto u ltimo, si el archivo pepe tuviese el siguiente contenido: 2.3 3 hola t o 4 5.89 cual ser a la salida del programa?
12.5. Ejemplo
Para jar ideas vamos a mostrar a continuaci on un ejemplo completo en el que se trabaja con archivos. En los primeros tiempos de la inform atica s olo exist an terminales en modo texto, por lo que cuando alg un programa ten a que representar gr acamente una funci on se recurr a frecuentemente a usar la impresora como salida gr aca. Vamos a retroceder por tanto unos cuantos a nos en la historia de la inform atica y vamos a realizar un programa que, partiendo de un archivo en el que est an escritas las notas de los alumnos de programaci on, genere otro archivo con un histograma de las notas. En dicho histograma una barra representar a el n umero de ocurrencias en el archivo de la nota correspondiente. Por ejemplo si el archivo de notas es el siguiente: 7.1 7.2 9.2 6.5 8.3 8.4 7.5 6.7 5.6 9.4 10 9 8 5.5 5.6 5.0 6.7
12.5. EJEMPLO
6.4 6.2 6.0 6.1 7.0 7.9 7.6 7.7 8.5 10
135
lo cual indica que todos los alumnos han estudiado un mont on, el programa generar a el siguiente archivo: 0 1 2 3 4 5 6 7 8 9 10
La forma de interpretar esta gr aca es la siguiente: como en la l nea del 5 hay cuatro asteriscos, esto indica que hay cuatro notas entre 5 y 6 (sin incluir el 6) o dicho de una manera m as formal, el n umero de notas contenidas en el intervalo 5 6 es cuatro. La excepci on es el 10; su l nea indica el n umero de notas iguales a 10. El programa a realizar se puede dividir en varias tareas:
Abrir los archivos. Leer el archivo de notas y contar cuantas notas est an dentro de un intervalo dado. Para ello se usar a un vector de enteros en el que en su primer elemento almacenaremos el n umero de notas contenidas en el intervalo 0 1 , en el segundo las contenidas en 1 2 y as sucesivamente. La excepci on ser a el elemento n umero 10 que s olo almacenar a las notas iguales a 10.
Escribir en un archivo el histograma. Para ello no hay m as que imprimir una l nea para cada intervalo, en la que se escribir an tantos asteriscos como notas haya en dicho intervalo (lo cual est a indicado en el n umero almacenado en el elemento del vector correspondiente a dicho intervalo). La primera tarea se ha realizado dentro de la funci on main(), pero las otras dos se han realizado en las funciones CuentaNotas() y EscribeHisto() respectivamente. El listado del programa completo se muestra a continuaci on:
1 /* Programa: ImpGraf 2 * 3 * Descripci on: Lee un archivo que contiene las notas de los alumnos (una
136
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
#include <stdio.h> void CuentaNotas(FILE *pfnot, int histo[]); void CreaHisto(FILE *pfhis, int histo[]); void main(void) { int histo_not[11]; /* Vector para almacenar el n umero de ocurrencias de la nota correspondiente. Si histo_not[9] vale 50 es que 50 alumnos han sacado una nota entre 9 y 10 ( animo chicos/as) */ FILE *pfnotas; /* Puntero al archivo de notas */ FILE *pfhisto; /* Puntero al archivo de histograma */ /* Se abren los dos ficheros */ pfnotas = fopen("notas","r"); if(pfnotas == NULL){ printf("Error: No se ha podido abrir el fichero \"notas\"\n"); exit(1); } pfhisto = fopen("histogra","w"); if(pfhisto == NULL){ printf("Error: No se ha podido abrir el fichero \"histogra\"\n"); fclose(pfnotas); /* cierro el fichero que ya hab a abierto */ exit(1); } CuentaNotas(pfnotas, histo_not); CreaHisto(pfhisto, histo_not); fclose(pfnotas); fclose(pfhisto); } /* * * * * * * * * Funci on: CuentaNotas() Descripci on: Lee las notas del fichero cuyo puntero se pasa como argumento y crea el vector con el histograma. Argumentos: FILE *pfnot: Puntero al archivo de notas. int histo[]: Histograma. Revisi on 0.0: 25/05/1998.
12.5. EJEMPLO
137
58 * 59 * Autor: Jos e Daniel Mu noz Fr as. 60 */ 61 62 void CuentaNotas(FILE *pfnot, int histo[]) 63 { 64 int n; /* N umero de datos le do por fscanf() */ 65 int i; /* Indice para los for */ 66 double nota_act; /* Almacena la nota que se acaba de leer del fichero */ 67 68 /* Inicializo la matriz del histograma */ 69 for(i=0; i<11; i++){ 70 histo[i] = 0; 71 } 72 73 n = fscanf(pfnot, "%lf", ¬a_act); 74 while(n!=EOF && n!=0){ /* termina en cuanto ocurra un error de lectura */ 75 if( (nota_act < 0) || (nota_act > 10) ){ /* Se verifica que las notas 76 sean v alidas */ 77 printf("Error en el fichero de entrada. Las notas han de estar entre" 78 "0 y 10\n"); 79 exit(1); 80 } a la nota */ e casilla est 81 for(i=0; i<11; i++){ /* Se busca en qu 82 if( (i <= nota_act) && (nota_act < i+1) ){ 83 histo[i]++; 84 break; /* una vez que he situado la nota en su casilla salgo del for */ 85 } 86 } 87 n = fscanf(pfnot, "%lf", ¬a_act); 88 } 89 } 90 91 /* Funci on: CreaHisto() 92 * 93 * Descripci on: Representa "gr aficamente" en un fichero el histograma que 94 * se pasa en el vector histo. Para ello por cada elemento de 95 * dicho vector se transforma en una fila de asteriscos (un 96 * asterisco representa una unidad) 97 * 98 * Argumentos: FILE *pfhis: Puntero al archivo de notas. 99 * int histo[]: Histograma. 100 * 101 * Revisi on 0.0: 25/05/1998. 102 * 103 * Autor: Jos e Daniel Mu noz Fr as. 104 */ 105 106 void CreaHisto(FILE *pfhis, int histo[]) 107 { 108 int i; /* Indice para los for */ 109 int contador; /* cuenta los asteriscos escritos */ 110 111 for(i=0; i<11; i++){
138
112 113 114 115 116 117 118 119 120 121 } 122 }
Ejercicios 1. Modicar el programa anterior para que sea f acil cambiar el car acter con el que se imprime el histograma. Para ello usar la sentencia #define (por ejemplo #define CAR_HISTO #). 2. Realizar lo mismo que en el ejercicio anterior pero pidi endole al usuario al principio del programa el car acter con el que se desea que se imprima el histograma. 3. Realizar un programa que a partir de un archivo que contiene una serie de n umeros (en el mismo formato que el usado para el archivo de notas) calcule la media y la varianza de la serie y la imprima por pantalla. Para ello se aconseja usar funciones que lean el archivo y que devuelvan el resultado de sus c alculos. As por ejemplo para calcular la media se podr a crear una funci on con el prototipo double media(FILE *pfich); 4. Realizar un programa que a partir del archivo de notas cree un archivo con el histograma de notas y a continuaci on imprima la media y la varianza de las notas. Nota: Para volver
a leer un archivo desde el principio se puede usar la funci on rewind(), cuyo prototipo es: void rewind(FILE *pfich);
5. Realizar un histograma en el que los intervalos representados sean de medio punto, es decir, se tendr a una la para las notas contenidas en 0 0 5 , otra para 0 5 1 y as sucesivamente. Para ello usar una sentencia #define para crear un par ametro que indique el n umero de intervalos en que se divide el conjunto de notas.
139
La funci on fgetc() lee el siguiente car acter desde el archivo como un unsigned char y lo devuelve convertido a int. Si se llega al nal del chero u ocurre alg un error el valor devuelto es EOF5 acter (convertido a unsigned char) que se le pasa La funci on fputc() escribe el car como argumento en el archivo al que apunta puntero a archivo. El valor devuelto es el car acter escrito (convertido a int) o EOF si ha ocurrido alg un error. fgets() lee una cadena de caracteres del archivo puntero a archivo y los almacena en la cadena de caracteres cadena. La lectura se acaba cuando se encuentra el car acter de nueva l nea \n (que se escribe en la cadena), cuando se encuentra el n de chero (en este caso no se escribe \n en la cadena) o cuando se han le do tam cad 1 caracteres. En todos estos casos se escribe un car acter \0 en la cadena a continuaci on del u ltimo car acter le do. Por supuesto la cadena de caracteres ha de tener espacio suciente como para almacenar todos los caracteres que se puedan leer en la funci on (tam cad). El valor devuelto es un puntero a la cadena le da o NULL si ha ocurrido alg un error o se ha llegado al nal del chero. fputs() escribe la cadena en el archivo al que apunta puntero a archivo sin el \0 del nal. El valor devuelto es un n umero positivo si se ha escrito la cadena correctamente o EOF en caso de error. Para ilustrar el uso de estas funciones vamos a estudiar un ejemplo:
Imprimir un archivo Vamos escribir en C un programa que imprima por pantalla el contenido de un chero cuyo nombre se pide al usuario al principio del programa (el funcionamiento del programa ser a similar al del comando type de MS-DOS). El programa, despu es de abrir el archivo, leer a car acter a car acter el contenido del archivo e ir a imprimiendo dichos caracteres por pantalla. Un pseudoc odigo del programa es el siguiente Pedir al usuario el nombre del archivo que se ha de imprimir. Mientras no se llegue al final del archivo{ leer un car acter del archivo imprimir el car acter } Cerrar el archivo. Y un programa que realiza esta tarea se muestra a continuaci on.
1 /* Programa: ImpArch 2 * 3 * Descripci on: Imprime el contenido del archivo cuyo nombre se pide al 4 * usuario por la pantalla.
5 Puede parecer un poco extra no el leer un car acter y devolverlo como un int. La raz on de esto es la de poder distinguir los caracteres normales de los errores, pues si se devolviese el car acter le do como un unsigned char, al ocupar la tabla ASCII todos los valores que se pueden representar mediante un unsigned char, no quedan valores libres para representar el EOF. La soluci on es devolver un entero en el que los caracteres ocupan de 0 a 255 de forma que el resto de valores puedan usarse para devolver c odigos de error.
140
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 * * Revisi on 0.0: 26/05/1998 * * Autor: Jos e Daniel Mu noz Fr as. */ #include <stdio.h> #define TAM_CAD 256
main(void) { char nom_fich[TAM_CAD]; /* Nombre del fichero que se va a abrir */ char letra; /* car acter le do desde el fichero y escrito en pantalla */ FILE *pfich; /* Puntero al fichero que se imprime */ printf("Introduzca el nombre del fichero que desea imprimir: "); gets(nom_fich); pfich = fopen(nom_fich, "r"); if(pfich == NULL){ printf("Error al intentar abrir el fichero \"%s\"\n", nom_fich); exit(1); } letra = fgetc(pfich); while(letra != EOF){ printf("%c", letra); letra = fgetc(pfich); } fclose(pfich); }
Cap tulo 13
Estructuras de datos
13.1. Introducci on
En muchas situaciones un objeto no puede describirse por un solo dato, sino por una serie de ellos. Por ejemplo el historial m edico de un paciente consta de varios campos, siendo cada uno de ellos es de un tipo distinto. Un ejemplo de historial m edico, junto con los tipos de datos utilizados podr a ser el mostrado en la siguiente tabla: Dato Nombre Edad Peso No Seguridad Social Historia Tipo de dato Cadena de caracteres N umero entero N umero en punto otante N umero entero Cadena de caracteres Tipo en C char[100] unsigned short float unsigned long char[1000]
Si se desea realizar un programa para gestionar una base de datos con los historiales de los pacientes, ser a mucho m as c omodo poder agrupar todos los datos de un paciente bajo una misma variable compuesta. En C estos tipos de variables denidos como una agrupaci on de datos, que pueden ser de distintos tipos, se denominan estructuras. Hasta ahora hab amos visto que C dispon a de unos tipos b asicos para manejar n umeros y caracteres y que a partir de estos tipos b asicos se pod an crear vectores y matrices, que eran agrupaciones de datos pero de un mismo tipo. Las estructuras de datos son un paso m as all a, al permitir agrupar en una misma variable datos no solo de un mismo tipo, sino de tipos variados, tal como se va a mostrar en este cap tulo.
142 };
Esta declaraci on no reserva espacio en la memoria para la estructura; simplemente crea una plantilla con el formato de la estructura. La sintaxis para denir una variable del tipo estructura previamente declarado es: struct nombre de estructura variable; Esta sentencia reservar a un espacio en la memoria para albergar una estructura nombre de estructura, a la cual se podr a acceder mediante la etiqueta variable. Por ejemplo la declaraci on de una estructura para almacenar el historial m edico de un paciente se escribir a: struct paciente{ char nombre[100]; unsigned short edad; float peso; unsigned long n_seg_social; char historia[1000]; }; Y para crear una variable del tipo estructura paciente se escribe: struct paciente pepito; N otese que una vez declarada una estructura, dicha estructura se puede considerar como un nuevo tipo de dato con el nombre struct nombre de estructura, y que la declaraci on de variables de ese nuevo tipo de dato es id entica a la del resto de variables; comp arense si no las dos deniciones siguientes: unsigned long numero; struct paciente juanito;
143
Lo cual aparte del ahorro de espacio en la denici on de enteros de dos bytes tiene la ventaja de que si se desea portar el programa a una m aquina en la que los enteros de dos bytes son el tipo int s olo habr a que cambiar la declaraci on de WORD al principio del programa, dejando intacto el resto de deniciones. Tambi en se puede usar typedef para simplicar la denici on de variables de tipo estructura. Para ello se puede unir la declaraci on de la estructura con una denici on de tipos de la forma: typedef struct paciente{ char nombre[100]; unsigned short edad; float peso; unsigned long n_seg_social; char historia[1000]; }PACIENTE; La denici on de variables de tipo estructura paciente se podr a realizar ahora como: PACIENTE pepito; Lo cual mejora bastante la legibilidad del programa. En resumen el uso de typedef tiene dos nalidades: Permite una mejor documentaci on del programa a dar nombres m as signicativos a los tipos, especialmente a los derivados. Mejora la portabilidad del programa, al poder encapsular tipos que dependan de la m aquina en sentencias typedef.
144
Como puede apreciarse, en primer lugar se ha declarado la estructura complejo y se le ha asociado el tipo COMPLEJO1 (l neas 1316). N otese adem as que dicha declaraci on se ha realizado antes de comenzar la funci on main(). Esto u ltimo permite que el formato de la estructura se pueda usar en todas las funciones del programa. Si por el contrario la declaraci on
1 El nombre de la estructura y el nombre del tipo no tienen por qu e coincidir, aunque es conveniente para evitar confusiones.
145
de la estructura complejo se hubiese escrito dentro de la funci on main(), dicha declaraci on ser a privada a la funci on main() y por tanto no se podr a usar fuera de dicha funci on. En segundo lugar es necesario destacar que el uso de los miembros de una estructura es id entico al de una variable que tenga su tipo. Por ejemplo en la l nea 25 se usa el operador & para obtener la direcci on del miembro real de la estructura comp1 para que scanf() pueda escribir en dicha variable el n umero que introduzca el usuario por el teclado. Conviene destacar tambi en que la precedencia del operador punto es mayor2 que la del operador & y por tanto la direcci on que se obtiene es la del miembro de la estructura.
Como se recordar a en las llamadas a funciones los argumentos de la funci on se copian en unas variables locales y el valor devuelto por la funci on tambi en se copia a la variable a la que se asigna la funci on en el programa principal. Por tanto una funci on para inicializar una estructura del tipo struct complejo se puede escribir: struct complejo InicComplejo(double real, double imag) { struct complejo resultado; resultado.real = real; resultado.imag = imag; return resultado; } Para llamar a la funci on desde un programa se escribir a simplemente: ... struct complejo comp1; ... comp1 = InicComplejo(2.0, 3.7); ... Como se puede apreciar la funci on recibe dos n umeros reales y crea un n umero complejo llamado resultado. Dicha estructura al terminar la funci on es devuelta al programa principal, en donde se copia a la estructura comp1, que es tambi en una estructura de tipo complejo.
2 De hecho el operador punto, junto con el operador corchete ([ ]) para referenciar elementos de matrices y el operador echa (->) que se estudiar a mas adelante, son los operadores con la mayor precedencia en C.
146 Ejercicios
1. Suponiendo que se ha denido con typedef la estructura struct complejo como COMPLEJO, tal como se hizo en el ejemplo de la secci on 13.5; modicar los ejemplos anteriores para usar el nuevo tipo COMPLEJO. El paso de par ametros es similar. Por ejemplo una funci on que devuelva la suma de dos complejos que se pasan como argumentos ser a: COMPLEJO SumaComp(COMPLEJO c1, COMPLEJO c2) { COMPLEJO res; res.real = c1.real + c2.real; res.imag = c1.imag + c2.imag; return res; } Ejercicios 1. Modicar el programa de la secci on 13.5 para que la suma de los dos n umeros complejos se realice mediante una llamada a la funci on SumaComp(). Sin embargo el paso de estructuras de un tama no elevado a una funci on es un proceso costoso, tanto en memoria como en tiempo. Por ello el paso de estructuras a funciones s olo debe de realizarse cuando estas sean de peque no tama no, tal como se acaba de hacer con la estructura COMPLEJO. Por ejemplo si se quiere pasar a una funci on una estructura del tipo PACIENTE, descrita en la secci on 13.3, se copiar an 1110 bytes. En estos casos se opta siempre por pasar a la funci on un puntero a la estructura en lugar de la estructura completa, del mismo modo que con los vectores y matrices.
147
Para acceder a un elemento de una estructura a trav es de un puntero se utiliza el operador de indirecci on *, al igual que con el resto de variables, as si ppaciente apunta a una estructura de tipo paciente, *ppaciente es la estructura y (*ppaciente).edad es la edad del paciente. Si en el ejemplo anterior se desea imprimir la cha del paciente usando el puntero ppaciente hay que escribir: printf("Paciente %s:\n Edad: %d\n Peso: %f\n N Seg Soc. %ld\n" " Historia: %s\n", (*ppaciente).nombre, (*ppaciente).edad, (*ppaciente).peso, (*ppaciente).n_seg_social, (*ppaciente).historia); En estos casos los par entesis son estrictamente necesarios, pues tal como se dijo antes, el operador punto tiene una precedencia mayor que el operador *. Si no se ponen los par entesis *ppaciente.edad acceder a a la direcci on de memoria contenida en edad, es decir, ser a equivalente a *(ppaciente.edad). Como edad no es un puntero sino un entero, el programa acceder a a una posici on de memoria indeterminada y que adem as no le pertenecer a, provoc andose un grave error. Como los punteros a estructuras se usan con bastante frecuencia en C, existe una notaci on alternativa para acceder a un miembro de una estructura mediante un puntero, que es el operador echa ->3 . As por ejemplo, las dos asignaciones siguientes son equivalentes: (*ppaciente).edad = 32; ppaciente->edad = 32;
Y una funci on para inicializar una estructura del tipo PACIENTE pidi endole los datos al usuario ser a:
void InitPaciente(PACIENTE *ppaciente) { printf("Nombre del paciente: "); gets(ppaciente->nombre);
3 El
operador echa est a formado por un - (s mbolo menos) y un > (s mbolo mayor que)
148
printf("Edad del paciente: "); scanf("%d", &ppaciente->edad ); printf("Peso del paciente: "); scanf("%f", &ppaciente->peso ); printf("N de la Seguridad Social: "); scanf("%ld", &ppaciente->n_seg_social ); while(getchar()!=\n); /* scanf() termina de leer en cuanto termina el n umero, dejando el \n del final de la cadena que introduce el usuario en el buffer de lectura. Si no se eliminan mediante este bucle los caracteres que scanf no ha le do, dichos caracteres ser an le dos por gets y se almacenar an en la historia del paciente */ printf("Historial del paciente: "); gets(ppaciente->historia); }
Por u ltimo, un programa que muestra c omo se llamar a a ambas funciones es:
#include <stdio.h> typedef struct paciente{ char nombre[100]; unsigned short edad; float peso; unsigned long n_seg_social; char historia[1000]; }PACIENTE; void InitPaciente(PACIENTE *ppaciente); void ImpriPaciente(PACIENTE *ppaciente); main() { PACIENTE paciente; InitPaciente(&paciente); printf("\nLos datos del paciente son:\n\n"); ImpriPaciente(&paciente); }
149
es crear dos vectores de estructuras de tipo COMPLEJO con las que se trabajar a mucho m as c omodamente. La declaraci on de un vector de estructuras es id entica a la de un vector de cualquier otro tipo de dato. Por ejemplo para crear un vector de 4 estructuras de tipo COMPLEJO basta con escribir: COMPLEJO vector_complejo[4]; Y para acceder a los miembros de una estructura que a su vez es un elemento del vector se hace: vector_complejo[0].real vector_complejo[0].imag ... vector_complejo[3].real vector_complejo[3].imag = 4.3; = 23.27; = 2.1; = 3.7;
N otese que al igual que el resto de vectores la numeraci on de los elementos comienza en 0. Conviene tener en cuenta que al igual que con el resto de los vectores, al denir un vector de estructuras se reserva un espacio suciente en la memoria para contener a las estructuras que componen el vector y se crea un puntero constante que apunta al comienzo de dicha zona de memoria. En este ejemplo vector complejo es un puntero constante de tipo COMPLEJO * que apunta al principio del vector de cuatro estructuras de tipo COMPLEJO.
150
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 typedef struct complejo{ double real; double imag; }COMPLEJO;
/* Prototipos de las funciones */ void PideVec(COMPLEJO *pvec, int dim); COMPLEJO ProdEsc(COMPLEJO *pvec1, COMPLEJO *pvec2, int dim); void main(void) { COMPLEJO vec1[MAX_DIM], vec2[MAX_DIM]; /* Vectores introducidos por el usuario */ COMPLEJO res; /* Resultado del producto escalar */ int dim; /* Dimensi on de los vectores*/ printf("Introduzca la dimensi on de los vectores: "); scanf("%d", &dim); printf("\nIntroduzca los complejos del vector 1\n"); PideVec(vec1, dim); printf("\nIntroduzca los complejos del vector 2\n"); PideVec(vec2, dim); res = ProdEsc(vec1, vec2, dim); printf("\nEl producto escalar es: %lf + %lf i\n", res.real, res.imag); } /* Funci on: PideVec() * on dim umeros complejos de dimensi on: Pide al usuario un vector de n * Descripci * * Argumentos: COMPLEJO *pvec1: Puntero al vector de estructuras de complejos * donde se escribir an los n umeros introducidos * por el usuario. * int dim: Dimensi on del vector de complejos. */ void PideVec(COMPLEJO *pvec, int dim) { int n; for(n=0; n<dim; n++){ printf("Parte real del elemento %d: ", n); scanf("%lf", &(pvec[n].real) ); printf("Parte imaginaria del elemento %d: ", n); scanf("%lf", &(pvec[n].imag) ); } } /* Funci on: ProdEsc() *
151
70 * Descripci on: Calcula el producto escalar de dos vectores de n umeros 71 * complejos 72 * 73 * Argumentos: COMPLEJO *pvec1: 74 * COMPLEJO *pvec2: Punteros a los vectores de complejos. 75 * int dim: Dimensi on de los vectores de complejos. 76 * 77 * Valor devuelto: COMPLEJO: Resultado del producto escalar. 78 */ 79 80 COMPLEJO ProdEsc(COMPLEJO *pvec1, COMPLEJO *pvec2, int dim) 81 { 82 int n; 83 COMPLEJO resul; 84 85 resul.real = 0; /* Inicializaci on del resultado */ 86 resul.imag = 0; 87 88 for(n=0; n<dim; n++){ 89 resul.real += pvec1[n].real*pvec2[n].real - pvec1[n].imag*pvec2[n].imag; 90 resul.imag += pvec1[n].real*pvec2[n].imag + pvec1[n].imag*pvec2[n].real; 91 } 92 return resul; 93 }
La funci on main() consta simplemente de las deniciones de los vectores de estructuras (linea 27) y de las llamadas a las funciones PideVec() y ProdEsc(), que es donde se realiza todo el proceso del programa. La funci on PideVec() se encarga de pedir al usuario las partes real e imaginaria de cada elemento del vector y de almacenarlas en vector cuya direcci on se le pasa como argumento (pvec). N otese que se ha usado el operador corchete para acceder a cada uno de los elementos del vector. Por u ltimo la funci on ProdEsc() se encarga de calcular el producto escalar mediante la f ormula:
dim 1
i 0
V1 i V2 i
En donde V1 i y V1 i son las partes real e imaginaria del elemento i del vector de complejos V1 . Para calcular el sumatorio de la f ormula anterior, en cada iteraci on del bucle for de las l neas 8891 se calcula un sumando de dicho sumatorio y se acumula dicho sumando en el n umero complejo resul, que previamente se ha inicializado a cero (l neas 8586). La funci on devuelve (mediante copia) el valor de dicho n umero complejo al programa principal. Ejercicios 1. Modicar el programa principal para impedir que el usuario introduzca una dimensi on err onea para los vectores: si el usuario intenta introducir una dimensi on mayor del tama no m aximo del vector MAX DIM o menor o igual a 0, se dar a un mensaje de error y se volver a a pedir la dimensi on de nuevo.
V1 i V2 i
V1 i V2 i
V1 i V2 i i
152
2. Modicar el programa principal para crear los vectores din amicamente. Una vez conocida la dimensi on de estos, se asignar a memoria para los dos vectores mediante una llamada a la funci on calloc(). 3. Modicar la funci on ProdEsc() para trabajar con el operador echa -> en lugar del operador corchete [] para acceder a los elementos de los vectores de complejos en el c alculo del producto escalar.
---> --->
En denitiva los archivos de tipo texto son legibles porque s olo contienen caracteres como n umeros, letras y signos de puntuaci on. Las funciones fprintf y fscanf en encargan de hacer las traducciones de formato num erico a caracteres y viceversa. Los archivos binarios se utilizan para almacenar la informaci on como una copia exacta de la memoria del ordenador. Por ejemplo, si una variable entera vale 2563 estar a almacenada en la memoria del ordenador en forma de 2 bytes (en un PC con Windows sizeof(int) vale 2). De acuerdo a la codicaci on de los n umeros enteros estos dos bytes ser an (en hexadecimal) el 0A y el 03 o lo que es igual el 0000 1010 y el 0000 0011 en formato binario. Estos bits son los que realmente se encuentran almacenados en la memoria. Cuanto este n umero entero se guarda en un archivo binario se guardan 2 bytes dentro del archivo (como ya se ha dicho ser an los bytes 0A y 03). Como norma general los archivos binarios no se pueden ver con un editor de texto, ya que ser a una casualidad que los bytes que contienen fuesen caracteres legibles. Siguiendo con el ejemplo anterior, ni el car acter 0A ni el car acter 03 son imprimibles. Tambi en se puede decir que los archivos binarios y los programas que utilizan archivos binarios no son f acilmente portables a otros ordenadores u otros compiladores. Esto es evidente ya que el tama no de las variables no es siempre igual, una variable tipo int ocupa 2 bytes en un PC con sistema operativo Windows, pero normalmente
153
ocupa 4 bytes en una estaci on de trabajo con sistema operativo Unix. Adem as, no es s olo un problema de tama no sino que unos procesadores ordenan la memoria de una manera (primero 0A y luego 03) y otros al rev es (primero 03 y luego 0A). Es importante tener en cuenta todas estas caracter sticas de los archivos binarios cuando se adaptan programas que funcionan en una conguraci on determinada a otra conguraci on distinta.
154
for (i=0; i<N; i++) { fprintf(f,"%s\n",producto[i].nombre); fprintf(f,"%6d\n",producto[i].precio); } fclose(f); } El archivos datos.txt que produce el programa anterior tiene cuatro l neas porque se ha puesto un car acter de cambio de l nea al nal de cada instrucci on fprintf. El archivo tiene el siguiente aspecto: CD Rosana 2563 CD Titanic 2628 Los n umeros no aparecen ajustados al margen izquierdo porque se ha puesto un formato %6d que escribe dos espacios delante de cada n umero de cuatro cifras. El archivo tiene en total 35 caracteres que son los siguientes: CD Rosana
Nota: Se han representado los espacios como y los caracteres de cambio de l nea como para que se pueda leer mejor.
Ejercicios 1. Comprobar que cada estructura escribe un n umero diferente de caracteres en el archivo. 2. Cu al ser a el archivo resultante si los fprintf fuesen los siguientes?: fprintf(f,"%15s\n",producto[i].nombre); fprintf(f,"%-6d\n",producto[i].precio); Cu antos caracteres ocupar a el archivo?
155
/* Programa: binario.c Descripci on: Inicializa un vector de dos elementos tipo estructura y escribe todos los datos en un archivo binario. on 0.0: 1/jun/1999 Revisi Autor: El programador binario. */ #include <stdio.h> /* printf, fwrite, fopen, fclose */ #include <stdlib.h> /* exit */ #include <string.h> /* strcpy */ #define N 2 /* Dimensi on de los vectores (n umero de productos) */ /* Declaraci on de una estructura para los productos */ typedef struct { char nombre[12]; int precio; }PROD; void main(void) { PROD producto[N]; /* Vector de productos en el almac en */ int i; /* contador para los bucles*/ FILE *f; /* Descriptor del archivo de texto */ /*** Inicializo del vector ***/ strcpy(producto[0].nombre,"CD Rosana"); producto[0].precio=2563; strcpy(producto[1].nombre,"CD Titanic"); producto[1].precio=2628; /*** Creo el archivo ***/ f=fopen("datos.dat","wb"); if (f==NULL) { printf("Error al crear el archivo datos.dat\n"); exit(1); } for (i=0; i<N; i++) { fwrite(&producto[i], sizeof(producto[i]),1,f); } fclose(f); } Notar que al abrir el archivo se especica el modo wb, la b indica acceso binario. El archivos datos.dat que produce el programa anterior no puede verse en un editor de texto porque contiene caracteres especiales. Ocupa en total 28 caracteres ya que cada estructura ocupa 14 bytes, que son 12 caracteres del primer nombre, y 2 bytes para el int del precio. Por lo tanto el archivo puede representarse como: CD Rosana ????CD Titanic ???
156
Nota: Se han representado los espacios como , los caracteres \0 de nal de cadena como y otros caracteres no imprimibles como ?. En este caso no hay cambios de l nea. Los 12 primeros caracteres corresponden al nombre del primer producto, que es un vector de char de longitud 12 de los cuales s olo 10 caracteres est an inicializados (9 de textttCD Rosana y el 10o el car acter NULL que pone la funci on strcpy) por lo tanto luego aparecen 2 caracteres desconocidos. A continuaci on viene el n umero 2563 codicado como entero que ocupa 2 bytes (que son el 0A y el 03 en hexadecimal). Otra manera de representar el contenido del archivo es la siguiente:
0A 0A
03 44
1. Comprobar que en lugar del bucle for se pod a haber escrito la siguiente l nea: fwrite(&producto[0], sizeof(producto[0]),N,f); Esta instrucci on escribe las N estructuras del vector producto de una sola vez. 2. Cu antos caracteres ocupar a el archivo si el precio se guardase en una variable tipo long en lugar de tipo int? 3. Cu al ser a el archivo resultante si el nombre del primer producto fuese CD-R" y el precio 200?
157
158
Bibliograf a
[Antonakos and Manseld, 1997] Antonakos, J. L. and Manseld, K. C. (1997). Programaci on estructurada en C. Prentice Hall Iberia, Madrid. [Delannuy, ] Delannuy, C. El Libro de C como Primer Lenguaje. Eyrolles, ediciones Gesti on 2000 S. A., Barcelona. [Kernighan and Ritchie, ] Kernighan, B. W. and Ritchie, D. M. El lenguaje de programaci o n C. Prentice Hall, segunda edici on. [Roberts, 1995] Roberts, E. S. (1995). The Art and Science of C. Addison Wesley.
159