E l C e s e l le n g u a je d e p ro g ra m a c ió n d e lo s o c h e n ta : e s rá p id o , e fic ie n te , c o n c is o , e s tru c tu ra d o y fá c il d e tra n s p o rta r d e u n o s o rd e n a d o re s a o tro s . H o y e n d ía , e l 7 0 p o r 1 0 0 d e l software p a ra o rd e n a d o re s p e rs o n a le s e s tá s ie n d o d e s a rro lla d o e n C . P R O G R A M A C IO N E N C . In tro d u c c ió n y c o n c e p to s a v a n z a d o s , e s u n a g u ía p rá c tic a q u e p o n e e n s u s m a n o s to d o lo q u e s e n e c e s ita s a b e r p a ra c o m e n z a r a u s a r e s te e x c ita n te le n g u a je . S e rá , a d e m á s , m a n u a l d e c o n s u lta p a ra q u ie n e s s e s ie n ta n fa s c in a d o s p o r la s p o te n c ia lid a d e s d e e s te le n g u a je , y a q u e tra ta ta m b ié n lo s a s p e c to s m á s a v a n z a d o s d e l le n g u a je C : e l p re p ro c e s a d o r C , u tiliz a c ió n d e e s tru c tu ra s , c a m b io s d e tip o , o p e ra c io n e s c o n fic h e ro s y m a n e jo d e b its . S e c u b re ta m b ié n e l u s o d e l C e n e n to rn o U N IX , la u tiliz a c ió n d e p u e rto s e n e l 8 0 8 6 /8 0 8 8 , e l re e n v ío d e e n tra d a / s a lid a , g rá fic o s , m ú s ic a , m a c ro s , e tc . P R O G R A M A C IO N E N C . In tro d u c c ió n y c o n c e p to s a v a n z a d o s e s tá b a s a d o e n la im p la n ta c ió n e s tá n d a r d e l C d e K e rn ig h a n & R itc h ie e n e l IB M P C .
Programación en C In tro d u c c ió n y c o n c e p to s a v a n z a d o s
M itc h e ll W a ite S te p h e n P ra ta D o n a ld M a rtin
Indice
Prólogo ..................................................................................................................... 9 1. Preparados...
Listos... ¡C!.......................................................................... 13
¿De dónde viene?—¿C para qué?—¿Adonde va?—Uso del C.—Algunas convenciones.—Un consejo. 2. Introducción
al C........................................................................................ 27
Un ejemplo sencillo de C.—Explicación.—Estructura de un programa sencillo.—Claves para hacer legible un programa.—Subiendo un nuevo peldaño.—Y mientras estamos en ello...—Hasta ahora hemos aprendi do.—Cuestiones y respuestas.—Ejercicios. 3. Los datos en
C........................................................................................... 45
Datos: variables y constantes.—Tipos de datos.—Tipos de datos en C.—Utilización de tipos de datos.—Hasta ahora hemos aprendido.— Cuestiones y respuestas. 4. Tiras de caracteres, #define, printf( ).......................................................... 71 Introducción a las tiras de caracteres.—Constantes: el preprocesador C.—Usos y utilidades de printf( ) y scanf( ).—Claves de utilización.— Hasta ahora hemos aprendido.—Cuestiones y respuestas. 5
5. Operadores, expresiones y sentencias................................................. 97 Introducción.—Operadores fundamentales.—Algunos operadores adi cionales.—Expresiones y sentencias.—Conversiones de tipo.—Un pro grama ejemplo.—Hasta ahora hemos aprendido.—Cuestiones y respues tas.—Ejercicios. 6. Funciones de entrada/salida y reenvío................................................ 133 E/S de un solo carácter: getchar( ) y putchar( ) .—Buffers.—Otra eta pa.—Reenvío.—UNIX.—E/S dependiente de sistema: puerto de E/S 8086/8088.—Vamos a tantear la potencia oculta de nuestro ordena dor.—Hasta ahora hemos aprendido.—Cuestiones y respuestas.— Ejercicios. 7. Una encrucijada en el camino.............................................................. 163 La sentencia if.— La sentencia if con else.— Quién es el más grande: ope radores de relación y expresiones.—Seamos lógicos.—Un programa pa ra contar palabras.—Una caricatura con caracteres.—El operador con dicional: ?:.— Elección múltiple: switch y break.— Hasta ahora hemos aprendido.—Cuestiones y respuestas. 8. Bucles y tirabuzones.............................................................................. 203 El bucle while. —Algoritmos y seudocódigo.—El bucle for. —Un bucle con condición de salida: do while.— ¿Con qué bucle nos quedamos?— Bucles anidados.—Otras sentencias de control: break, continue, goto. — Arrays.—Una cuestión sobre entradas.—Resumen.—Hasta ahora hemos aprendido.—Cuestiones y respuestas.—Ejercicios. 9. Funcionamiento funcional de las funciones........................................ 243 Creación y utilización de una función sencilla.—Argumentos de funcio nes.—Devolución de un valor desde una función: return. —Variables lo cales.—Localización de direcciones: el operador &.—Alteración de va riables en el programa de llamada.—A ver cómo funcionamos.—Cómo especificar tipos de funciones.—Todas las funciones C se crean de la mis ma manera.—Resumen.—Hasta ahora hemos aprendido.—Cuestiones y respuestas.—Ejercicios.
12. Arrays y punteros....................................... ........................................... 331 Arrays: Punteros a arrays.—Funciones, arrays y punteros.— Suplantación de arrays por punteros.—Operaciones con punteros.— Arrays multidimensionales.—Punteros y arrays multidimensionales.— Hasta ahora hemos aprendido.—Cuestiones y respuestas.—Ejercicio. 13. Tiras de caracteres y funciones relacionadas..................................... 357 Definición de tiras dentro de un programa.—Entrada de tiras.—Salida de tiras.—La opción «hágaselo usted mismo».—Funciones de tiras de caracteres.—Ejemplo: ordenación de tiras.—Argumentos en líneas de ejecución.—Hasta ahora hemos aprendido.—Cuestiones y respuestas.— Ejercicios. 14. Estructuras de datos y otras lindezas.................................................. 391 Problema ejemplo: Creación de un inventario de libros.—Puesta a pun to del patrón de la estructura.—Definición de variables de estructura.— Cómo acceder a miembros de la estructura.—Arrays de estructuras.— Estructuras anidadas.—Punteros a estructuras.—Cómo enseñar estruc turas a las funciones.—Y después de las estructuras, ¿qué?—Un vistazo rápido a las uniones.—Otro vistazo a typedef.—Hasta ahora hemos aprendido.—Cuestiones y respuestas.—Ejercicios. 15. La biblioteca C y el fichero de entrada/salida.................................... 421 Cómo acceder a la biblioteca C.—Funciones de biblioteca que ya he mos utilizado.—Comunicación con ficheros.—¿Qué es un fichero?— Un programa sencillo de lectura de ficheros: fopen( ), fclose( ), getc( ) y putc( ).—Un programa sencillo de reducción de ficheros.—Fichero E/S: fprint( ), fscanf( ), fgets( ) y fputs( ).—Acceso aleatorio: fseek( ).—Comprobación y conversión de caracteres.—Conversiones de tiras de caracteres: atoi( ), atof( ).—Salida: exit( ).—Asignación de me moria: malloc( ) y calloc( ).—Otras funciones de biblioteca.— Conclusión.—Hasta ahora hemos aprendido.—Cuestiones y respues tas.—Ejercicios. APENDICE A: Lecturas adicionales.......................................................... 447
10. Modos de almacenamiento y desarrollo de programas.....................277 Modos de almacenamiento: Propósito.—Una función de números alea torios.—Lanza los dados.—Una función para atrapar enteros: getint( ) .—Ordenación de números.—Resumen.—Hasta ahora hemos aprendido.—Cuestiones y respuestas.—Ejercicios.
APENDICE B: Palabras clave en C........................................................... 449
11. El preprocesador C............................................................................... 313
APENDICE C: Operadores C..................................................................... 451
Constantes simbólicas: #define.— Utilización de argumentos con #define. —¿Macros o funciones? Inclusión de un fichero: #include.— Otros comandos: #undef, #if, #ifdef, #ifndef, #else y #endif.— Hasta ahora hemos aprendido.—Cuestiones y respuestas.—Ejercicio.
Lenguaje C.—Programación.—El sistema operativo UNIX.
Palabras clave de control de programas.
APENDICE D: Tipos de datos y modos de almacenamiento . . . . . . . . . . 4 5 5 Tipos de datos básicos.—Cómo declarar una variable simple.—Modos de almacenamiento. 7
APENDICE E: Control de flujo en el programa........................................... 459
La sentencia while.—La sentencia for.—La sentencia do while.— Utilización de sentencias if para elegir entre opciones.—Elección múlti ple con switch.—Saltos en el programa.
Prólogo
C es un lenguaje de programación sencillo y elegante, que se ha transfor mado rápidamente en el medio elegido por un número cada vez mayor de programadores para comunicarse con su ordenador. Este libro (Programa ción en C, por si ha perdido la tapa) pretende ser una guía sencilla para apren der y un libro de consulta, más adelante, para aquellos que se sientan fasci nados por las potencialidades de este lenguaje. En el subtítulo se recalca que es éste un libro para los que empiezan. Con ello queremos indicar que nuestro primer objetivo es guiar al lector en sus primeros pasos por los vericuetos básicos del C. En programación, la expe riencia es el gran maestro; por ello encontrará en el libro multitud de ejem plos con los que jugar y estudiar. Hemos utilizado figuras allá donde hemos creído que ayudaban a aclarar un determinado punto. De tanto en tanto, se resumen y destacan las características fundamentales del C, para hacerlas fá ciles de localizar. Hay también cuestiones (y respuestas) que permiten com probar lo que hemos avanzado. En principio, no suponemos que el lector posee un conocimiento al dedillo de ningún otro lenguaje clásico de progra mación, pero sí comparamos de vez en cuando este lenguaje con los demás, con el fin de ayudar a aquellos lectores que sí dominan alguno. La segunda parte del subtítulo del libro pretende indicar que, además de manual de principiantes, el libro contiene bastantes más cosas. Lo primero, la sección de cuestiones y respuestas apuntada anteriormente. Además, in tentando llegar más lejos de lo que un primer manual alcanza, discutiremos 9
algunos de los aspectos más avanzados del C, como utilización de estructu ras, cambios de tipo, operaciones con ficheros y, en el apéndice, manejo de bits en el C, así como algunas extensiones del propio lenguaje. El libro cubre también el entorno del C en microordenadores y UNIX; por ejemplo, se dis cute el reenvío de entrada/salida en ambos entornos, y se comenta la utiliza ción de los puertos en microprocesador 8086/8088. Los dibujos y pequeñas historietas introducidos pueden considerarse también como extra; por cier to, un extra bastante agradable. Hemos intentado, por encima de todo, hacer esta introducción al C útil, instructiva y clara. Para sacar a este libro todo el partido posible deberá us ted, lector, jugar un papel lo más activo posible. No se limite a leer los ejem plos; antes bien, introdúzcalos en su sistema e intente hacerlos funcionar ade cuadamente. El C es un lenguaje de programación muy fácil de transportar de un sistema a otro, pero quizá encuentre alguna diferencia marginal en la forma de ejecución de programas en su sistema con respecto al nuestro. No se canse de experimentar; cambie alguna parte del programa que esté ejecu tando y observe el efecto producido; modifique el programa para hacerle rea lizar una tarea ligeramente diferente; haga caso omiso de nuestras adverten cias en un momento determinado para ver lo que sucede; intente realizar el mayor número posible de cuestiones y ejercicios. Cuanto más ponga de su parte, más aprenderá. Le deseamos la mejor de las suertes en el camino de aprendizaje de C. Hemos intentado que el libro se acople a sus necesidades, y esperamos que, por su parte, alcance los objetivos que se ha propuesto. Mitchell Waite Stephen Prata Donald Martin
Preparados... listos... ¡C! En este capítulo encontrará: • • • •
¿De dónde viene? ¿C para qué? ¿Adonde va? Uso del C • Uso de un editor para la preparación del programa • Ficheros fuente y ficheros ejecutables • Compilación de C en un sistema UNIX • Compilación de C en un IBM PC (Microsoft/Lattice C) • Otra forma distinta de compilar • Algunas convenciones • Un consejo
13
Preparados... listos... ¡C! CONCEPTOS Historia del C Virtudes del C Lenguajes compilados Ejecución de un programa C
Por supuesto, suponemos que la mayor parte de los lenguajes pretende) ser útiles, pero a menudo establecen otros objetivos adicionales. Por ejem plo, uno de los objetivos principales del PASCAL es proporcionar una base sólida para enseñanza de los principios de programación. El BASIC, por si parte, se desarrolló intentando asemejarse al inglés, de manera que fuese fá cilmente comprendido por estudiantes no familiarizados con ordenadores ( si son de habla inglesa, mejor). Todos estos objetivos, evidentemente, son importantes, pero no siempre son compatibles con la utilidad pura y simple El C ha sido creado como herramienta de programación, manteniendo, ade más, una justa fama de lenguaje «amistoso» para el programador.
¿C para qué? El lenguaje C se está transformando rápidamente en una de las bases de programación más importantes y populares. Esta creciente utilización se de be a que la gente lo prueba y le gusta; conforme vaya avanzando en su apren dizaje, también usted se sentirá atraído por sus numerosas virtudes. Mencionaremos a continuación algunas de ellas. El C es un lenguaje moderno, que incorpora las características de control apuntadas como deseables por la teoría y práctica de la informática. Su pro pio diseño hace que resulten naturales para el usuario aspectos como la pla nificación escalonada, programación estructurada y diseño modular; el re sultado es un programa más fiable y comprensible.
Bienvenido al mundo del C. En este capítulo le ayudaremos a prepararse para emprender el aprendizaje de este poderoso lenguaje, cada vez más po pular. ¿Qué necesita para estar listo? En primer lugar, necesita interesarse por el C; suponemos que ya ha asumido este punto. No obstante, trataremos de aumentar su interés exponiendo brevemente algunos de los aspectos más seductores del C. En segundo lugar, necesita una guía que le introduzca en el lenguaje; para eso está este libro. Además, necesita tener acceso a un orde nador que posea un compilador C; esto lo tendrá que arreglar por su cuenta. Por último, necesita saber cómo ejecutar un programa C en su sistema; le daremos algunos consejos acerca de este particular al final del capítulo.
¿ D e d o n d e v ie n e ? El C fue creado por Dennis Ritchie, de los Laboratorios Bell, en 1972, cuando trabajaba, junto con Ken Thompson, en el diseño del sistema opera tivo UNIX. Por otra parte, el C no surgió por generación espontánea del ce rebro de Ritchie; se deriva del lenguaje B de Thompson, el cual, a su vez..., pero eso es otra historia. Lo importante es que el C se creó como herramien ta para programadores. En consecuencia, su principal objetivo es ser un len guaje útil. 15
El C es un lenguaje eficiente. Su diseño aprovecha las “habilidades” de los ordenadores al uso. Los programas C tienden a ser compactos y ejecutar se con rapidez. El C es un lenguaje portátil. Con ello queremos significar que los progra mas C escritos en un sistema pueden ejecutarse en otros sin ninguna modifi cación, o con modificaciones mínimas. En este último caso, a menudo las modificaciones se reducen a cambiar unas cuantas sentencias de entrada en un fichero de encabezamiento (header) que acompaña al programa princi pal. Por supuesto, la mayor parte de los lenguajes pretenden ser portátiles; sin embargo, cualquiera que haya intentado convertir un programa en BA SIC IBM PC a Apple BASIC (y eso que son primos hermanos), o que haya intentado ejecutar un programa FORTRAN en un gran ordenador IBM con sistema UNIX, sabrá que la cosa no es tan sencilla; de hecho, aparecen gran número de pequeños detalles que pueden crear problemas. En este sentido, el C es un líder en lenguajes portátiles. Existen compiladores C para unos 40 sistemas, que abarcan desde microprocesadores de 8 bits hasta el actual campeón mundial de velocidad en ordenadores, el Cray 1. El lenguaje C es también poderoso y flexible (dos de las palabras favoritas en la bibliografía de ordenadores). Por ejemplo, la mayor parte del sistema operativo UNIX, poderoso y flexible (¿lo ve?) como pocos, está escrita en C. Incluso están escritos en C los compiladores e intérpretes de otros lengua jes, como FORTRAN, APL, PASCAL, LISP, LOGO y BASIC. Así pues, cuando utilice FORTRAN en una máquina UNIX recuerde que, a la postre, hay un programa C que está haciendo el trabajo de producción del programa ejecutable final. Se han utilizado programas C para resolver problemas físi cos e ingenieriles, e incluso para producción de secuencias animadas en pelí culas como El Retomo del Jedi. El C posee control sobre aspectos del ordenador asociados generalmente con lenguajes ensambladores. Si lo desea, puede «afinar» al máximo sus pro gramas para lograr la mayor eficiencia. El C es un lenguaje “amistoso”. Es lo suficientemente estructurado para ejercer buenos hábitos de programación, pero en ningún caso le encorseta en un mare mágnum de restricciones. Podríamos citar más virtudes y, sin duda, algunos defectos. En lugar de profundizar más en el asunto, vayamos a la siguiente pregunta.
Estructuras flexibles de control
Formato estructurado
GASTOS DE INSTALACION: 10.000 pts.
BASIC GASTOS DE INSTALACION: 100.000 pts
¿a donde va? El C es ya el lenguaje predominante en el mundo de los miniordenadores de sistemas UNIX; actualmente, se está extendiendo a los ordenadores per sonales. Muchas compañías de software están utilizando C con preferencia a otros lenguajes en sus programas: procesado de textos, hojas electrónicas, compiladores, etc. Estas compañías saben que el C produce programas com pactos y eficientes, y, lo que es más importante, saben también que estos pro gramas son fáciles de modificar y de adaptar a nuevos modelos de ordenadores.
Programas reducidos código compacto Figura 1.1
Virtudes del C
Pascal GASTOS DE INSTALACION: 50.000 pts.
Transportable a otros ordenadores
JUEGOS DE ORDENADOR EL RETORNO
DEL
JEDI
SISTEMA OPERATIVO UNIX
LENGUAJE MINIORDENADORES
MICROORDE NADORES
Paquetes de SOFTWARE
LENGUAJES ORDENADOR PROGRAMAS DE GESTION F igu ra 1.2.
El C se usa para...
Otro factor que contribuye a la diseminación del C hacia el mundo de los ordenadores personales es la actitud de los usuarios de C UNIX, que de sean poder llevar a casa sus programas C. Actualmente existen ya varios com piladores C que les permiten hacerlo. Pensamos que lo que es bueno para las compañías y para los veteranos del C debe serlo también para otros usuarios. Hay cada vez más programadores que utilizan C simplemente para aprovechar sus ventajas. No hay que ser un profesional de los ordenadores para utilizarlo. En resumen, el C está destinado a ser uno de los lenguajes más importan tes de esta década y de los años noventa. Se utiliza en miniordenadores y en ordenadores personales. Lo usan compañías de software, estudiantes de in formática y entusiastas de todas clases. Por cierto, si desea buscar un trabajo escribiendo software, una de las primeras preguntas a las que deberá respon der “sí” es: “¿De manera que sabe usted C?”
c El C es un lenguaje “compilado”. Si no le suena esta palabra, no se preo cupe; vamos a explicarle lo que significa conforme vayamos describiendo las etapas necesarias para producir un programa C. Si es usuario de un lenguaje compilado como PASCAL o FORTRAN, encontrará familiares las etapas básicas necesarias para echar a andar un pro
grama C. Si, por el contrario, su formación informática se basa en un lenguaje “intérprete”, como BASIC o LOGO, o si carece por completo de formación en ningún lenguaje, encontrará el proceso un poco extraño al principio. Afor tunadamente, estamos aquí para guiarle a lo largo del mismo, y se dará cuenta de que, en realidad, es bastante directo y lógico. Daremos, en primer lugar, un repaso rápido del proceso. En síntesis, lo que debe hacer desde el momento que comienza a escribir el programa hasta ejecutarlo es: 1-. Utilizar un “editor” para escribir el programa C. 2. Enviar el programa a su amigo el compilador. Este comprobará si su programa tiene algún error, y, en su caso, se lo hará saber. En caso contrario, el compilador acometerá la tarea de traducir el programa al lenguaje interno de su ordenador, y colocará la traducción en un nuevo fichero. 3. A continuación, ya puede ejecutar el programa tecleando el nombre de este nuevo fichero. En algunos sistemas, la segunda etapa puede estar subdividida, a su vez, en dos o tres subetapas, pero la idea sigue siendo la misma. A continuación daremos una batida más profunda de cada una de las etapas apuntadas arriba. Uso de un editor para la preparación del programa
A diferencia del BASIC, el C no posee su propio editor. En su lugar, uti lice un editor de propósito general que esté disponible en su sistema. En un sistema UNIX, por ejemplo, podría ser ed, ex, edit, emacs o vi. En un siste.ma de ordenador personal, puede ser ed, edling, Wordstar, Volkswriter o cual quier otro de entre los muchos que existen. Con algunos de estos editores, tendrá que especificar una opción particu lar. Por ejemplo, si utiliza Wordstar, deberá usar la opción N, opción de “no documento”. Las dos misiones principales que tiene a su cargo son: teclear el programa correctamente y escoger un nombre para el fichero en que almacene dicho programa. Las reglas que se siguen para este nombre son muy simples: debe ser un nombre permitido en su sistema y debe terminar con .c. He aquí dos ejemplos. ordena.c suma.c
Escoja la primera parte del nombre de manera que le recuerde lo que ha ce el programa. La segunda parte (.c) identifica el fichero como programa C. En el mundo mágico de los ordenadores, la parte del nombre que va seguida de un punto se denomina una “extensión”. Se utilizan las extensiones para informar al ordenador (y a usted mismo) sobre la naturaleza del fichero. 19
He aquí un ejemplo: utilizando un editor, preparamos el siguiente pro ama y lo almacenamos en el fichero informe .c. #include
main()
}
printf("Se usa .c para acabar
un fichero ce p ro gram a C \n");
El texto que acabamos de teclear se llama “código fuente”, y se guarda un “fichero fuente”. Es importante aclarar aquí que nuestro fichero fuente el comienzo de un proceso, no el final.
Ficheros fuente y ficheros ejecutables
Nuestro programa, maravilloso sin lugar a dudas, resulta, sin embargo, totalmente incomprensible para el ordenador. Un ordenador no entiende co sas como #include o printf. Lo único que entiende es “código máquina”, que son aberraciones tales como 10010101 y 01101001. Si queremos que el ordenador se muestre cooperativo, deberemos traducir nuestro código (códi go fuente) a su código (código máquina). El resultado de nuestros esfuerzos será un “fichero ejecutable”, que es un fichero relleno con todo el código máquina que ejecuta el ordenador para realizar su trabajo. Este asunto de la traducción puede parecer tedioso: no se preocupe. Nos las hemos arreglado para asignar el trabajo de traducción al propio ordena dor. Existen programas muy inteligentes, llamados “compiladores”, que se encargan del trabajo sucio. Los detalles del proceso dependen de cada siste ma en particular; a continuación, veremos algunos de ellos. Compilación de C en un sistema UNIX
El compilador C de UNIX se llama cc. Lo único que tenemos que hacer para compilar nuestro programa es teclear. cc informe.c
Transcurridos unos segundos, aparecerá un mensaje de UNIX para co municarnos que nuestros deseos han sido cumplidos (podemos también en contrar advertencias y mensajes de error si no hemos escrito el programa co rrectamente; supongamos, por el momento, que se realizó todo bien). Si ahora utilizamos ls para listar nuevos ficheros, encontraremos que ha aparecido un nuevo fichero llamado a.out. Este es el fichero ejecutable que contiene las traducciones (o “compilación”) de nuestro programa. Para ejecutarlo, sim plemente teclee a.out
y nuestra sabiduría se ve por fin recompensada: Se usa .c para acabar
un fichero de p ro gram a C
El programa cc combina varias etapas en una. Se comprende este punto con mayor claridad cuando realizamos el mismo proceso en un ordenador personal. Compilación de C en un IBM PC (Microsoft/Lattice C) F igu ra 1.3
Intérpretes y compiladores
Las etapas concretas que se han de seguir aquí dependen del sistema ope rativo y del compilador. Utilizaremos como ejemplo un compilador Micro soft C soportado en un PC DOS 1.1. (El compilador Lattice C, en el que 21
o simplemente informe.
conseguiremos ejecutar el programa.
se basa la versión Microsoft, utiliza el mismo formato: simplemente usa lc1 y 1c2 en lugar de mc1 y mc2.) Comenzamos de nuevo con un fichero llamado informe.c. Nuestra pri mera orden es
Figura 1.5
Preparación de un programa C en Microsoft/Lattice C
mc1 informe
El compilador interpreta informe como informe.c. Si todo va con nor malidad, esta orden produce un informe intermedio llamado informe.q. Te clee a continuación mc2 informe
lo que producirá el fichero llamado informe.obj. Este fichero contiene el “có digo objeto” (código en lenguaje máquina) en nuestro programa. Más ade lante volveremos sobre este punto. Teclee después link c informe
lo que producirá el fichero llamado informe.exe. Este era nuestro objetivo, un fichero ejecutable. Si ahora tecleamos informe.exe
En realidad, no tiene por qué aprender qué está sucediendo en este pro cedimiento; aun así, por si le interesa, comentaremos los puntos más impor tantes. ¿Qué hay aquí de nuevo? Desde luego, el fichero informe.obj es nuevo, Es un fichero en código máquina; la pregunta que surge inmediatamente es: ¿por qué no hemos parado aquí? La respuesta es que el programa complete incluye partes que no han sido escritas. Por ejemplo, utilizamos en el progra ma algunas subrutinas estándar de la biblioteca C. Así, el programa necesi tará tomar estas subrutinas de donde se hallen almacenadas. Esta misión la realiza el comando link que hemos introducido. Link forma parte del sistema operativo IBM DOS. Su misión es concate nar nuestro código objeto (informe.obj) con un fichero que contiene algunas utilidades estándar (c.obj) y buscar la biblioteca que hemos especificado; en este caso, lc.lib. A continuación enlaza todos los elementos para producir el programa final. El programa cc del UNIX pasa por una secuencia similar de etapas; lo que sucede en este caso es que la secuencia queda inadvertida, porque el pro23
pio objeto se borra cuando ya no es necesario. (Pero, si se lo pedimos con educación, nos proporcionará el código objeto con el nombre informe.o.) Otra forma distinta de compilar
Algunos compiladores de C adaptados a ordenadores personales utilizan un camino diferente. El método que hemos discutido hasta ahora produce un fichero de código objeto (extensión .obj) y utiliza el linker del sistema para producir un fichero ejecutable (extensión .exe). El método alternativo es generar un fichero de “código ensamblado’’ (extensión .asm) y utilizar a continuación el “ensamblador” del sistema para producir un fichero ejecutable. ¡Pero bueno, otro código más! El código ensamblador está estrechamente relacionado con el código máquina. De hecho es simplemente una repre sentación mnemotécnica del mismo. Por ejemplo, JMP podría significar 11101001, que es parte de un código máquina que instruye al ordenador para que salte (jump, en inglés) a un sitio diferente. (Si se imagina que con esto queremos decir que el ordenador salte de la mesa al suelo, está usted en un error; nos referimos a un salto en una dirección de memoria diferente.) Los humanos encuentran el código ensamblador mucho más digerible y fácil de recordar que el código máquina puro; el programa ensamblador, por su par te, se encargará de realizar la correspondiente traducción. Pero, ¿por qué?
Aquellos de ustedes que utilicen BASIC se estarán preguntando el moti vo de todas estas etapas preliminares a conseguir ejecutar el programa; pue den parecer simplemente una pérdida de tiempo; de hecho, pueden llegar a ser una pérdida de tiempo. Sin embargo, una vez que el programa ha sido compilado, se ejecutará mucho más rápidamente que un programa BASIC estándar. Así pues, tenemos que sortear algunos inconvenientes con el fin de conseguir un programa más eficiente como producto final.
convenciones Estamos ya casi listos para empezar. Lo único que queda es mencionar algunas convenciones que utilizaremos. Tipo de letra Cuando se pretenda en el texto representar programas, entradas y salidas de ordenador, nombre de ficheros, y variables, utilizaremos un tipo de letra que se asemeje al que se puede observar en una pantalla o impresora. Ya lo hemos utilizado algunas veces antes de este punto; en caso de que le haya pasado inadvertido, el tipo de letra tiene esta apariencia:
Color
Estamos utilizando un color azul para representar las respuestas y deman das que componen el funcionamiento interactivo entre el ordenador y el usua rio. También utilizamos el color azul en los resúmenes, con el fin de hacerlo más localizables. Periféricos de entrada y salida
Hay muchas formas por las que se puede comunicar un ordenador con un usuario como usted, por ejemplo. Supondremos en adelante que los comandos se introducen desde teclado, y las salidas del ordenador se leen en pantalla. Teclas
En general, se envía al ordenador una línea completa de instrucciones, apretando como final de línea una tecla que, dependiendo del sistema, está marcada como “enter”, “c/r” o “return”, en minúsculas o mayúsculas. Nos referiremos a esta tecla con la notación [enter]. Con ello queremos indicar que lo que debe hacer es pulsar esta tecla, y no teclear e-n-t-e-r. También nos referiremos a los caracteres de control de la forma [control-d]. Esta notación significa pulsar la tecla [d] manteniendo apretada la tecla “control”. Nuestro sistema
Hay algunos aspectos del C, tales como la cantidad de espacio utilizado para almacenar un número, que dependen del sistema utilizado. Cuando de mos ejemplos, aludiremos frecuentemente a “nuestro sistema”: nos estamos refiriendo a un IBM PC con sistema operativo DOS 1.1 y utilizando un com pilador Lattice C. En alguna ocasión trabajaremos también con programas realizados en un sistema UNIX. En este caso nos referimos a un ordenador VAX 11/750 equi pado con una versión UNIX BSD 4.1 de Berkeley.
Un consejo La mejor manera de aprender a programar es programar, no limitarse a leer. Hemos incluido en el libro multitud de ejemplos. Debe intentar ejecutar algunos de ellos en su sistema, para adquirir una idea mejor de cómo funciona. Intente hacer modificaciones para ver lo que pasa. Trabaje con las cuestiones y ejercicios que aparecen al final de los capítulos. En resumen, procure ser un alumno curioso y emprendedor; con ello logrará aprender C en profundi dad y rápidamente. Bueno, ya está usted listo y nosotros también; pasemos al capítulo 2.
printf( " H o l a ! \ n ” ) ; 25
2 Introducción al C En este capítulo encontrará: • Un sencillo programa de C • Explicación • Primera pasada: resumen rápido • Segunda pasada: detalles • Estructura de un programa sencillo • Claves para hacer legible un programa • Subiendo un nuevo peldaño • Y mientras estamos en ello... • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios
num = 1 ; printf("Soy un modesto "); printf("ordenador.\n”); printf("Mi numero es el %d por ser el primero.\n",num);
Introducción al C }
CONCEPTOS Estructura de un programa sencillo Declaración de variables Utilización de comentarios Programa legible
OPERADORES
Si piensa que este programa imprime algo en la pantalla, le felicitamos, ¡ha acertado! Lo que probablemente no sabrá es la forma exacta en que va a aparecer el texto. Para averiguarlo, ejecutemos el programa y veamos lo que pasa. En primer lugar, deberá usar su editor, para crear un fichero que contenga este inocente conjunto de líneas. Deberá otorgar un nombre a este fichero; si está demasiado excitado para pensar en uno, utilice prog.c como nombre del fichero. Compile este programa a continuación. (Esperamos pacientemente mientras usted consulta el manual del compilador de su sistema.) Ejecute el programa. Si todo ha ido bien, la salida tendrá un aspecto como: Soy un modesto ordenador. Mi numero es el 1 por ser el primero
Desde luego, el resultado no es nada sorprendente. Pero, ¿qué sucede con los símbolos \ n y %d del programa? Por otra parte, algunas líneas del pro grama tienen un aspecto bastante extraño. Es el momento de una explicación.
¿Qué apariencia tiene un programa C? Quizá haya observado los peque ños ejemplos dados en el capítulo 1, o visto algún listado en otro sitio, y en cuentra este lenguaje con un aspecto un tanto extravagante, repleto de sím bolos como { y *ptr + + . Según vaya avanzando en el libro, encontrará que la aparición de estos y otros símbolos característicos del C le parecen menos extraños, más familiares y quizá incluso agradables. En este capítulo comen zaremos por presentar un programa ejemplo bastante sencillo y explicar lo que hace. Al mismo tiempo, exploraremos algunos de los rasgos básicos del C. Si echa de menos una explicación más detallada, no se preocupe; ya la en contrará en los capítulos siguientes.
Ejemplo sencillo de C Vamos a observar un programa sencillo en C. Admitimos de antemano que el ejemplo dado es deliciosamente inútil, pero nos sirve para resaltar al gunas características básicas de un programa C. Más adelante lo explicare mos línea a línea, pero antes observe el programa e intente averiguar lo que hace. #include main() /* un p r o g r a m a
{ int num;
se ncillo */
Explicación Haremos dos pasadas por el programa. En la primera aclararemos el sig nificado de cada línea, y en la segunda veremos más detalladamente algunas implicaciones y detalles dentro del mismo. Primera pasada: resumen rápido
#include —inclusión de otro fichero. Esta línea comunica al ordenador que debe incluir información que se en cuentra en el fichero stdio.h. m a i n ( ) — un nombre de función. Los programas C se componen de una o más “funciones”, las cuales son los módulos básicos del programa. En este caso concreto, el programa con siste en una sola función llamada main. Los paréntesis identifican main() como nombre de función. /* un programa sencillo */ —Un Comentario
Se pueden utilizar los símbolos /* y */ para encerrar comentarios. Los comentarios son notas que se introducen para hacer más claro el pro grama. Están pensados para el lector, y son ignorados por el ordenador. {—comienzo del “cuerpo” de la función Esta llave marca el comienzo de las sentencias que componen la función. 29
Esta sentencia imprime la frase comprendida entre las comillas: Soy un modesto printf ("ordenador. \n")- una nueva sentencia de escritura
Esta sentencia añade ordenador.
al final de la frase anterior. El símbolo \n es un código que indica al orde nador que salte a una nueva línea. printf("Mi numero es el %d por ser el primero. \n", num);
Esta línea imprime el valor de num (que es 1) dentro de la frase que está entre comillas. El símbolo %d indica al ordenador dónde y en qué forma debe imprimir el valor de num. } —final Tal como prometimos, el programa finaliza con una llave de cierre. Hagamos ahora un estudio más detallado del mismo programa. Segunda pasada: detalles #include
Figura 2.1 Anatomía de un programa C
La definición de la función terminará con una llave de cierre, }. int num;— una sentencia de declaración Esta sentencia anuncia que se utilizará una variable llamada num y que esta variable será de tipo entero (integer). num = 1 ;- una sentencia de asignación Esta sentencia asigna el valor 1 a num. printf ("Soy un modesto
");
—
una sentencia de escritura
El fichero stdio.h se suministra como parte del compilador C, y contiene información de aspectos relacionados con la entrada y salida de datos (co municaciones entre el programa y la terminal, por ejemplo). El nombre pro cede de “standard input/output header”, encabezamiento estándar de en trada/salida. (La gente del C llama encabezamiento a un conjunto de infor maciones que van en la parte superior de un fichero.) Algunas veces necesitará usted incluir esta línea, y otras, no. No le pode mos facilitar una regla segura, ya que la respuesta depende del programa y del sistema. En nuestro sistema no hubiésemos necesitado esta línea para es te programa en concreto; sin embargo, puede suceder que en el suyo sí sea necesaria. En cualquier caso, la inclusión de la línea no produce ningún efec to nocivo. En adelante, sólo la introduciremos cuando la línea sea realmente necesaria en nuestro sistema. Probablemente se estará preguntando por qué algo tan básico como la entrada y salida de información no está incluida automáticamente. Una res puesta válida podría ser que no todos los programas utilizan un paquete de E/S (Entrada/Salida), y la eliminación de cargas innecesarias forma parte de la filosofía del lenguaje C. Y ya que hablamos de ello, comentaremos que esta línea no es ni siquiera una sentencia del lenguaje C. El símbolo # la iden tifica como línea a ser manipulada por el “preprocesador’’ C. Tal como in ferirá por su nombre, el preprocesador realiza algunas tareas antes de comenzar a actuar el compilador. Más adelante aparecerán nuevos ejemplos de instruc ciones de preprocesador. main() 31
La verdad es que main es un nombre bastante abstruso, pero, en este ca so, no tenemos otra elección posible. Un programa C comienza su ejecución siempre con la instrucción que recibe el nombre de main ( ), es decir, princi pal. Todas las demás funciones podrán llevar nombres elegidos por nosotros, pero siempre ha de haber una función main ( ) para echar a andar el progra ma. ¿Y los paréntesis? Los paréntesis identifican a main ( ) como función; más adelante trataremos las funciones con profundidad. Por el momento nos limitaremos a repetir que las funciones son los módulos básicos de un pro grama C. Estos paréntesis, en general, incluyen información que está siendo tras pasada a la función. En nuestro ejemplo elemental no hay información algu na que pasar; por tanto, el contenido de los paréntesis es nulo. Por el mo mento, no se olvide de ponerlos; pero, por lo demás, no se preocupe por ellos. El fichero que contiene el programa tiene un nombre también; en este ca so, sí puede ser cualquier nombre elegido por nosotros en tanto en cuanto satisfaga las convenciones de su sistema y finalice con .c. Por ejemplo, pode mos utilizar perfecto.c o tonto.c en lugar de main.c como nombre de fichero para albergar nuestro programa. /* un programa sencillo */
Debe utilizar comentarios para hacer más comprensible el programa para los demás y para usted mismo. Una agradable propiedad de los comentarios C es que se pueden colocar en la misma línea que la sentencia que se desea acla rar. Si el comentario es más largo, se pueden colocar en su propia línea o extenderse por más de una. Cualquier cosa que comience por /* y termine por */ es ignorada por el compilador, lo cual está muy bien, ya que, por lo demás, los comentarios suelen ser bastante ininteligibles para un compilador C.
{y}: Las llaves indican el comienzo y final de una función. Unicamente se pue den utilizar llaves {} para este propósito, no siendo válidos los paréntesis ( ) ni los corchetes [ ]. También se pueden utilizar las llaves para encerrar un grupo de senten cias dentro del programa. Estas sentencias constituyen una unidad o “blo que”. Si tiene cierta familiaridad con el lenguaje PASCAL o ALGOL, ob servará que las llaves cumplen una función similar a las sentencias begin y end de estos lenguajes. int num ;
La “sentencia de declaración” es una de las características más impor tantes del C. Como ya se dijo anteriormente, en este caso concreto se decia rán dos cosas: primero, que en algún sitio de la función se utilizará una “va riable” con el nombre “num”. En segundo lugar, el prefijo int proclama que num es un entero, es decir, un número sin decimales. El símbolo punto y co ma del final de la línea identifica ésta como una sentencia C o instrucción.
El punto y coma es parte de la sentencia, y no simplemente un separador d sentencias, como sucede en PASCAL. La palabra int es una “palabra clave” C que identifica uno de los tipo básicos de datos en C. Se llaman palabras clave a aquellas que se utilizan dentro del lenguaje; encontrará una lista de palabras clave C en el apéndice. En C es obligatorio declarar todas las variables que se utilizan: con ello queremos decir que se debe suministrar una lista de todas las variables que se usarán más adelante, indicando en cada una de ellas a qué “tipo” pertenecen. La declaración de variables se considera en general como una Buena Idea Llegado a este punto, tendrá probablemente en mente tres preguntas. La primera, ¿qué alternativas tengo para elegir un nombre? La segunda, ¿que significa eso de tipos de datos? La tercera, ¿por qué hay que declarar las va riables? Hemos preparado dos apartados para responder a la primera y a la tercera preguntas. Por lo que se refiere a la segunda, trataremos de ella en el capítulo 3; aquí va un pequeño adelanto. El C maneja varias clases (o “tipos”) de datos: en teros, caracteres y “punto flotante”, por ejemplo. El hecho de declarar una variable como entero o como carácter permite al ordenador almacenar, localizar e interpretar adecuadamente el dato.
ELECCION DE NOMBRE Le sugerimos que utilice nombres con significado para las variables. Se pueden utilizar hasta ocho caracteres por nombre. (En realidad, se pueden usar más, pero en C se ignoran todos excepto los ocho primeros. Así, el ordenador no distingue escabeche y escabechina (!), ya que sus ocho prime ros caracteres son idénticos.) Por lo demás, se pueden utilizar como carac teres las letras minúsculas, las letras mayúsculas, los números y el símbolo de subrayar__, el cual cuenta como una letra. En todo caso, el primer ca rácter debe ser una letra. Nombres válidos Nombres no válidos pepe n o m b re 1
1n o m b re
M i_C a sa
M i-C a sa P aco's
_n u m ero
Las subrutinas de biblioteca utilizan a menudo nombres que comienzan con el símbolo de subrayar. Se hace así con la idea de que los usuarios no utilizarán generalmente nombres de este tipo; así existen pocas posibilida des de que se utilice accidentalmente el nombre de alguno de los ficheros de biblioteca. Es una buena política; por consiguiente, resistir la tentación de utilizar nombres que comiencen por dicho símbolo, evitando así el ries go de una “colisión” con la biblioteca del sistema.
32
CUATRO BUENAS RAZONES PARA DECLARAR VARIABLES 1. Una observación del conjunto de variables, estando todas ellas agrupadas, hace más fácil al lector la comprensión de la finalidad del programa. Se mejora más aún esta característica utilizando nombres de variables con significado (por ejemplo, tasa en lugar de r) y añadiendo comentarios para explicar el uso de las variables. Una disposición tal del programa es una de las primeras recetas del manual del buen programador. 2. Si se detiene a pensar en la sección de declaración de variables, inevita blemente deberá realizar una cierta planificación del programa antes de comenzar a escribirlo. Por ejemplo, ¿con qué información se puede eje cutar el programa? ¿Qué es lo que deseo exactamente que imprima? 3. La declaración de variables ayuda a prevenir uno de los errores de pro gramación más sutiles y difíciles de encontrar: el cambio accidental de una letra en el nombre de la variable. Por ejemplo, supongamos que en un determinado lenguaje, cuyo nombre nos reservamos, escribe la sen tencia: LO M O = 430.00
y, durante el programa, introduce equivocadamente:
num = 1 ;
P R EC IO = 0.150 * LO M O - 20.0
en donde accidentalmente ha sustituido la letra O por el número 0. El programa creará una nueva variable llamada L0M0, y utilizará cual quier valor que se le ocurra para ella (quizá cero, quizá basura). Por tan to, PRECIO tendrá un valor equivocado, y llevará un tiempo respetable encontrar qué ha sucedido en realidad. Esto no puede suceder en C (a menos que se sea lo suficientemente estúpido como para declarar dos variables con un aspecto tan semejante), ya que el compilador se encar gará de avisar que la variable L0M0 no está declarada. 4. Su programa en C no funcionará a menos que declare las variables. Si las demás razones discutidas hasta ahora no le han convencido lo sufi ciente, esperamos que ésta sea bastante elocuente.
La “sentencia de asignación” es una de las operaciones más básicas. En este caso concreto significa “dar a la variable num el valor 1”. La cuarta línea instruía al ordenador para que reservase espacio a la variable num; en esta línea se le da valor a dicha variable. Posteriormente podemos asignar a num un valor diferente, si lo deseamos; es por ello que decimos que num es una variable. Nótese que la sentencia se completa con un punto y coma.
OPERADOR DE ASIGNACION
Figura 2.2
La sentencia de asignación es una operación de las más básicas 35
printf ("Soy un modesto ") ; printf("ordenador-\n" );
printf("Mi numero es el %d por ser el primero.\n", num);
Estas sentencias utilizan una función C estándar llamada printf( ); los pa réntesis nos indican, como ya se dijo, que estamos tratando con una función. Todo lo que está encerrado entre ellos es información que se pasa desde nuestra función (main( ) a la función printf( ). Dicha información se denomina el “argumento” de una función, y en el primer caso, dicho argumento es “Soy un modesto”. ¿Qué hace la función printf( ) con este argumento? Obvia mente, observa lo que hay entre las dos comillas y lo imprime en la pantalla del terminal.
printf ( )
de otra forma, cuando se pulsa la tecla [enter], el editor abandona la línea en donde estaba y comienza con una nueva, dejando la línea anterior sin ter minar. El carácter nueva línea es un ejemplo de lo que se denomina una “secuencia de escape”. Se utiliza una secuencia de escape para representar caracteres di fíciles o imposibles de teclear. Como ejemplo se pueden nombrar, además: \t, para tabulados, y \b, para retroceso. En cualquiera de estos casos, la secuencia de escape comienza con el carácter barra-atrás, \ . Volveremos a este punto en el capítulo 3. Bien, ya hemos explicado por qué nuestras tres sentencias de escritura pro ducen sólo dos líneas: la primera instrucción no lleva carácter nueva línea dentro de ella. En la línea final aparece una nueva rareza: ¿qué ha sucedido con el %d cuando se imprime la línea? La salida de esta línea, recuérdese, es: M i n u m e r o e s e l 1 p o r s e r e l p r im e r o .
¡Ajá! Se ha sustituido el número 1 en el símbolo %d al imprimir la línea; precísamete 1 era el valor de la variable num. Aparentemente, %d se com porta como un acomodador que guarda el sitio en el que debe albergarse el valor de num. Esta línea es similar a la sentencia BASIC: P R IN T "M i num ero
es
el "; num ; "
por ser el primero."
printf ("QUIERO SALIR EN PANTALLA! \ n") Figura 2.3
printf( ) con un argumento
Esta línea nos sirve de ejemplo de “llamada” a una función en C. Unica mente necesitamos teclear el nombre de la función e incluir los argumentos necesarios entre los paréntesis. Cuando el programa alcanza esta sentencia, se transfiere el control a la función llamada printf( ) (en este caso). Cuando la función termina la tarea que tiene asignada, independientemente de la que sea, transfiere de nuevo el control al programa original. La siguiente línea se distingue de ésta en los dos caracteres \n incluidos dentro de las comillas. Observaremos que no forman parte de la salida en pantalla. ¿Qué ha sucedido? Sencillamente que \n es la instrucción de co mienzo de una nueva línea. Esta combinación \n representa, en realidad, un carácter único llamado carácter “nueva línea” (newline). Su significado es: “comienza una nueva línea ajustándose al margen izquierdo”. O, lo que es lo mismo, este carácter realiza la misma función que la tecla [enter] de un teclado típico. “Pero —se dirá usted— \n parecen dos caracteres, no uno.” Bien, en realidad son dos caracteres, pero representan un único carác ter, para el cual no hay tecla adecuada en el teclado. ¿Y por qué no usamos la tecla [enter]? Sencillamente, porque se interpretaría como una orden in mediata para el editor, no como una instrucción para ser almacenada. Dicho
La versión C, en realidad, hace algo más. El símbolo % avisa al progra ma que se va a imprimir una variable en esta posición; la letra d, por su par te, informa que la variable a imprimir es un número (digit). La función printf( ) permite elegir el formato de las variables entre varias opciones. De hecho, la f de la instrucción printf( ) está ahí para recordarnos que es una sentencia de impresión con formato.
Estructura de un programa sencillo Ahora que hemos visto un ejemplo concreto, estamos ya preparados pa ra dar unas pocas reglas generales sobre los programas en C. Un programa se compone de una colección de una o más funciones, de las cuales una de ellas debe llamarse main( ). Una función consta de un encabezamiento y de un “cuerpo”. El encabezamiento contiene cualquier tipo de sentencias de pre procesador, como #include, así como el nombre de la función. Se puede re conocer dicho nombre porque va seguido por unos paréntesis, dentro de los cuales puede o no haber parámetros. El cuerpo de la función está limitado por llaves, { }, y consiste en una serie de sentencias, cada una de las cuales termina en un punto y coma. Nuestro ejemplo tenía una sentencia de decla ración, que indicaba el nombre y tipo de la variable que íbamos a utilizar. A continuación aparecía una sentencia de asignación, en la cual se le daba 37
un valor a la variable. Por último, se incluían tres sentencias de escritura, compuestas en cada caso por llamadas a la función printf( ).
main() { int cuatro; cuatro 4
printf ( "% d\n",
cuatro) ; }
ENCABEZAMIENTO #¡nclude < stdio.h > Main ( )
Instrucciones de preprocesador Nombre de la función con argumentos
CUERPO int num; num = 1; print f("%d es un número maravilloso. \ n")
El compilador averigua dónde termina una sentencia y comienza la siguiente por medio de los puntos y coma introducidos; en cualquier caso, convendrá con nosotros que la lógica del programa aparece mucho más clara si se sigue la convención mencionada. Por supuesto, tampoco había mucha lógica que seguir en el ejemplo anterior, pero lo mejor es desarrollar las buenas costum bres desde el principio.
Sentencia de declaración Sentencia de asignación Sentencia de función
main () / * Pasa 4 docenas a huevos* /
{
USE COMENTARIOS ELIJA LOS NOMBRES USE ESPACIO UNA SENTENCIA POR LINEA
int huevos, docenas;
Figura 2.4
Una función tiene encabezamiento y cuerpo
docenas = 4; huevos = 12* docenas; printf ("Hay %d huevos en %d docenas!", huevos, docenas);
Figura 2.5
Claves para hacer legible un programa Es una buena práctica de programación hacer que los programas sean fá cilmente legibles. Con ello se consigue que el programa sea más fácil de com prender, y también de corregir o modificar en caso necesario. Recuerde que también se está ayudando a sí mismo, ya que en un futuro podrá seguir con facilidad el desarrollo del programa. Intentaremos darle a continuación una serie de consejos útiles en este sentido. Hasta ahora hemos mencionado dos claves importantes: escoger nombre de variables con significado y utilizar comentarios. Obsérvese que estas dos técnicas se complementan recíprocamente. Si le damos a una variable el nombre anchura, no necesitaremos añadir un comentario adicional que explique que esta variable representa una anchura. Otra técnica a utilizar es emplear líneas en blanco para separar las seccio nes de la función. Por ejemplo, en nuestro sencillo programa anterior hemos introducido una línea en blanco separando la sección de declaración de la sección de “acción” (asignación e impresión). La línea en blanco no era ne cesaria desde el punto de vista del lenguaje, pero es tradicional en C utilizarla. Una cuarta técnica que seguimos es usar una sentencia por línea. De nue vo nos encontramos con una convención, ya que no es obligatorio en C escri bir el programa de esta forma. De hecho, el C tiene lo que se denomina “for mato libre”. Se pueden poner varias sentencias en la misma línea o, por el contrario, espaciar una sentencia en varias líneas. El ejemplo siguiente es, en consecuencia, correcto:
Haga sus programas legibles
Subiendo un nuevo peldaño Nuestro primer ejemplo era realmente sencillo, y el siguiente no va a ser mucho más difícil. Es éste: main()
/* P asa 4 docenas a huevos */
{
int h uevo s, doce nas; docenas = 4; huevos = 12* docenas; p r in t f ( " H a y % d h u e v o s e n % d d o c e n a s ! " , h u e v o s , d o c e n a s ) ;
}
¿Qué hay de nuevo aquí? Primero, hemos declarado dos variables en lu gar de una. Todo lo que hemos necesitado es separar las dos variables (hue vos y docenas) por una coma en la sentencia de declaración. En segundo lugar, hemos realizado un cálculo. Hemos desafiado la tre menda capacidad de cálculo de nuestro sistema obligándole a multiplicar 4 por 12. En C, como en muchos otros lenguajes, el símbolo * indica una multiplicación. Por tanto, la sentencia huevos = 12 * docenas;
39
significa: “mírese el valor de la variable docenas, multipliquese por doce y asígnese el resultado de este cálculo a la variable huevos”. (A juzgar por esta parrafada, el español llano no es tan claro como el C puro y simple; ésta es una de las razones por las que desarrollamos lenguajes para ordenador.) Finalmente, hemos hecho un uso más elaborado de la sentencia printf( ). Si ejecuta el programa del ejemplo, la salida será algo así: Hay 48 huevos en 4 docenas !
Esta vez hemos hecho dos sustituciones. El primer %d que aparece dentro de las comillas se sustituye por el valor de la primera variable (huevos) en la lista que aparece a continuación de la parte entrecomillada; el segundo %d, por su parte, ha sido sustituido por el valor de la segunda variable (do cenas) de la lista. Obsérvese que la lista de variables a imprimir se coloca en la parte final de la sentencia. Este programa no tiene precisamente amplitud de miras, pero podría formar el núcleo de un programa para convertir docenas en huevos. Todo lo que necesitamos es poder asignar de alguna forma nuevos valores a nuestras variables; aprenderemos a hacerlo más adelante.
Y mientras estamos en ello... He aquí un nuevo ejemplo. Hasta ahora nuestros programas han utilizado la función estándar printf( ). Vamos a ver ahora cómo se puede incluir y utilizar una función de nuestra propia cosecha. main () {
}
printf ( "Llamare a la función mayordomo. \n" ) ; mayordomo () ; printf("Si. Traigame un cafe y el libro de C.\n");
mayordomo() { printf("Llamó el señor? \n");
}
el capítulo 9, pero queríamos adelantarles lo fácil que es crear e incluir nues tras propias funciones.
Hasta ahora hemos aprendido Se da a continuación un resumen del difícil (aunque no imposible) proce so de aprendizaje que habrá seguido con este capítulo, con los hechos más relevantes que, esperamos, haya aprendido. Se incluyen pequeños ejemplos cuando el espacio lo permite. Cómo llamar al fichero que contiene su programa: ojo.c, o negro.c, o resumen.c, etc. Qué nombre se debe utilizar en programas de una sola función: main( ) La estructura de un programa sencillo: encabezamiento, cuerpo, llaves, sentencias Cómo se declara una variable entera: int nombre______de__la__variable; Cómo asignar valor a una variable: nombre____ de__la__variable = 1024; Cómo imprimir una frase: printf (“Esto no es serio’’); Cómo imprimir el valor de una variable: printf (“%d”, nombre_______ de__ la__variable); El carácter nueva línea: \ n Cómo incluir comentarios en un programa: /* análisis de dividendos */
Cuestiones y respuestas A continuación se proponen algunas cuestiones para ayudarle a compro bar si ha comprendido el contenido de este capítulo. Cuestiones 1. Iznogud Bagdad Milyunanoches ha preparado el siguiente programa, y se lo pre senta a usted para que se lo revise. A ver si puede echarle una mano. include studio.h main{ } /* Escribe el numero de dias de una semana /* (
La salida es algo así: Llamare a la función mayordomo. Llamo el señor? Si. Tráigame un cafe y el libro de C.
La función mayordomo( ) se define de la misma forma que main( ), con su cuerpo encerrado entre llaves. La función se llama simplemente por su nombre, incluyendo los paréntesis. No volveremos a hablar de este tema hasta
int d d := 9 ; print (Hay d dias en una semana. );
2. Indicar cuál sería la salida de cada una de las siguientes sentencias, suponiendo que forman parte de un programa completo. a. printf("Yo tenia una ovejita Lucera."); printf ("Que de campanitas yo le he hecho un collar. \n") ; 41
b. p r i n t f ( " P a r a t e ,
oh Sol\nYo te saludo!");
C. p r in t f ( ' C u a n g r it a n \ n e s o s / m i a ld i t o s \ n " ) ; d . in t
num;
num = 2;
printf("%d +
%d
= %d", num, num, num + num);
Respuestas 1. Línea 1: comience la línea con un #; el nombre del fichero es stdio.h; además, este nombre debe ir entre símbolos < y > . Línea 2: utilice ( ), no { }; el final del comentario es */, no /*. Línea 3: utilice {, no (. Línea 4: la sentencia se completa con un punto y coma. Línea 5: ¡el Sr. I.B.M. ha conseguido hacer una línea correcta, la línea en blanco! Línea 6: utilice = y no : = en sentencias de asignación (aparentemente, el Sr. I.B.M. sabe un poco de PASCAL) la semana tiene 7 días, no 9 Línea 7: debería ser printf (“Hay %d días en una semana. \ n ” , d); Línea 8: no existe, pero debería haberla, con una llave de cierre, }. 2. a. Yo tenía una ovejita Lucera.Que de campanítas yo le he hecho un collar. (Obsérvese que no hay espacio tras el punto. Si hubiésemos deseado un espacio habría mos de utilizar “ Que en lugar de “Que). b. ¡Párate, oh sol! ¡Yo te saludo! (Obsérvese que el cursor se ha dejado al final de la segunda línea.) c. ¡Cuán gritan esos/nmalditos! (Obsérvese que la barra (/) no tiene el mismo efecto que la barra-atrás (\).) d. 2 + 2 = 4 (Obsérvese que cada %d se reemplaza por el correspondiente valor de la variable de la lista. Nótese también que el signo + significa adición, y que el cálculo se puede realizar dentro de una sentencia printf( ).)
Ejercicios Leer un libro de C no es suficiente. Debe intentar escribir uno o dos programas sencillos por sí mismo y comprobar si se ejecutan de forma correcta, al igual que los ejemplos del capítulo. Presentamos aquí algunas sugerencias, pero quizá prefiera utilizar sus propias ideas (nunca se sabe). 1. Escriba un programa que imprima su nombre. 2. Escriba un programa que escriba su nombre y dirección, utilizando tres o más líneas. 3. Escriba un programa que convierta su edad de años a días. Por el momento, no se preocupe de fracciones de años y de años bisiestos.
42
3 Los datos en C En este capítulo encontrará: • Datos variables y constantes • Tipos de datos • Enteros • Punto flotante • Tipos de datos en C • Tipos int, short y long • Declaración de tipos enteros • Constantes enteras • Inicialización de variables enteras • Utilización • Tipo unsigned • Tipo char • Declaración de variables de caracteres • Constantes de caracteres • Un programa • Tipos float y double • Declaración de variables de punto flotante • Constantes de punto flotante • Otros tipos • Tamaños de los tipos • Utilización de los distintos tipos de datos • Hasta ahora hemos aprendido • Cuestiones y respuestas
Los datos en C CONCEPTOS
mentarios. (Como referencia, hemos incluido el nombre del programa como comentario. Observaremos esta costumbre en futuros programas.) /* eldorado */ /* un programa para calcular su peso en oro */ main() {
float peso, valor; /* 2 variables en punto flotante */ char pita; /* una variable caracter */ pita = ' \007';/* asigna un caracter especial a pita */ printf("Vale ud. su peso en oro?\n") ; printf("Introduzca su peso en kg. y ya veremos.\n");
Programas interactivos Tipos básicos de datos Variables y constantes Declaración de los diferentes tipos Palabras, bytes y bits
PALABRAS CLAVE int,short, long, unsigned, char, float, double
}
scanf("%f", &peso); /* toma un dato del usuario */ valor= 400.0*peso*32.1512; /* supone que el oro se cotiza a 400$ la onza */ /* 32.1512 pasa kg. a onzas troy * / printf ( "%cSu peso en oro equivale a $%2.2f%c. \n", pita, valor, pita); printf("Seguro que ud. vale mucho mas! Si el oro baja, "); printf("coma mas\npara mantener su valor.\n");
OPERADORES sizeof
Los programas funcionan con datos. La misión de un ordenador, en prin:ipio, es “alimentarse” de números, letras y palabras, y a continuación manipular estos datos. En los dos siguientes capítulos nos concentraremos en los conceptos implicados en los datos y en sus propiedades. A continuación, nos meteremos con algunos datos y veremos qué podemos hacer con ellos, hablar de datos es muy poco divertido; por tanto, también haremos en este capítulo un poco de manipulación. Nos ocuparemos, en principio, de las dos grandes familias de tipos de datos: enteros y de punto flotante. El C ofrece unas cuantas variedades de estos tipos; aprenderemos cuáles son, cómo se declaran, cómo se utilizan y, muy importante, cuándo se utilizan. También se discutirán las diferencias entre constantes y variables. Empezaremos, una vez más, observando un programa ejemplo. Como siempre, aparecerán algunas arrugas poco familiares, que iremos planchando para usted a lo largo del capítulo. De todas formas, el propósito general del programa debe estar claro, de modo que lo mejor que puede hacer es intentar compilarlo y ejecutarlo. Para ahorrar tiempo, no introduzca los co
Cuando introduzca este programa, probablemente le interesará cambiar el valor 400.00 al precio actual del oro en dólares por onza. Sin embargo, sugerimos que no juguetee con la constante 32.1512, que representa el núme ro de onzas que hay en un kilogramo (nos referimos a onzas troy, utilizadas para metales preciosos y a kilogramos del sistema métrico decimal, utiliza dos para personas preciosas y de las otras). Observe que ha “introducido” su peso, tecleándolo, al ordenador y pulsando a continuación la tecla “enter” o “return”. Al pulsar esta tecla, el ordenador entiende que se ha termi nado de teclear la respuesta. Cuando ejecute el programa, la salida tendrá un aspecto como éste: Vale ud. su peso en oro? Introduzca su peso en kg. y ya veremos. 80
Su peso en oro equivale a $1028838.40. Seguro que ud. vale mucho mas! Si el oro baja, coma mas para mantener su valor.
El programa tiene también aspectos poco aparentes. Tendrá que ejecutar el programa por su cuenta para averiguar de qué se trata, aunque quizá el nombre de una de las variables dé una pista. ¿Qué hay de nuevo en este programa? 1. Habrá observado probablemente que hemos utilizado dos tipos nue vos en la declaración de variable. Con anterioridad habíamos usado sólo variables de tipo entero, pero ahora hemos añadido una variable de punto flotante y una variable carácter, de manera que podemos ma nejar una variedad de datos más amplia. 47
2. Hemos incluido algunas nuevas formas de escribir constantes. Ahora tenemos números con puntos decimales, y hemos utilizado una nota ción de aspecto bastante peculiar para representar el carácter llamado pita. 3. En la salida de estas nuevas clases de variables hemos usado los códi gos %f y %c en la función printf( ), con el fin de manejar variables de punto flotante y de carácter, respectivamente. Hemos utilizado mo dificadores al código para alterar la apariencia de la salida. 4. Quizá la novedad más llamativa de este programa es que es “interacti vo”. El ordenador le solicita información, y a continuación utiliza el número que usted le suministra. Un programa interactivo es más inte resante que los ejemplos no interactivos que hemos usado anteriormente; conviene destacar, además, que los planteamientos interactivos per miten realizar programas más flexibles. Por ejemplo, nuestro progra ma ejemplo se puede utilizar con cualquier peso razonable (y hasta no razonable), y no simplemente con 80 kilogramos. No hay necesidad de reescribir el programa cada vez que deseemos calcular el peso en oro de una nueva persona: las funciones scanf( ) y printf( ) permiten estas alteraciones. La función scanf( ) lee datos de teclado y los entre ga al programa. Ya vimos en el capítulo 2 que printf( ) lee datos del programa y los entrega a la pantalla. Si se manejan en equipo, estas dos funciones permiten establecer una comunicación de doble vía con el ordenador, lo que hace que la utilización de la máquina sea mucho más divertida.
variable, y 32.1512 es una constante. ¿Qué sucede con 400.0? Bien, el precio del oro no es constante en la vida real, pero nuestro programa lo trata como constante. La diferencia entre una variable y una constante es bastante obvia: una variable puede tener asignado su valor o cambiarlo durante la ejecución del programa; una constante, por el contrario, no puede variar. Esta diferencia hace que el manejo de variables sea un poco más complicado para el ordena dor y que consuma más tiempo su proceso; de todas maneras, nuestra mara villosa máquina puede con ello. /* el dorado * /
En este capítulo trataremos de los dos primeros apartados, variables y constantes de diversos tipos de datos. Los dos últimos puntos mencionados tratarán en el siguiente capítulo, pero continuaremos utilizando en éste las funciones scanf( ) y printf( ).
Datos: variables y constantes Un ordenador, bajo la dirección de un programa, puede realizar una enorme variedad de tareas diferentes. Se pueden sumar números, ordenar nomres, controlar un altavoz o pantalla, calcular órbitas de cometas, preparar na lista de correspondencia, dibujar muñecos, tomar decisiones o cualquier otra cosa que su imaginación consiga crear. Para realizar estas tareas, el pro grama necesita trabajar con “datos”, que son los números y caracteres que contienen la información a utilizar. Algunos de los datos están preseleccionados antes de la ejecución del programa y mantienen sus valores inalterados durante la misma; dichos datos se denominan “constantes”. Otros da tos pueden variar o pueden recibir nuevas asignaciones de valor durante la ejecución del programa; en este caso estaremos hablando de “variables”. (Ya hemos utilizado este término en el último capítulo; considere la última frase corno una presentación formal.) En nuestro programa ejemplo, peso es una
F igu ra 3.1
Funcionamiento de scanf ( ) y printf ( )
Tipos de datos Más allá de la distinción entre variables y constantes interesa la diferen cia entre los distintos “tipos” de datos. Existen datos numéricos; otros son letras o, en general, caracteres. El ordenador necesita un sistema para identi ficar y utilizar todas estas diferentes clases de datos. En C el sistema consiste en reconocer algunos “tipos de datos” fundamentales. Si el dato es una cons tante, el compilador es capaz, generalmente, de decirnos de qué tipo se trata simplemente por el aspecto que tiene; por el contrario, las variables necesi tan un anuncio previo de su tipo en una sentencia de declaración. Iremos com49
pletando los detalles conforme avancemos; por el momento, observemos los tipos de datos fundamentales reconocidos en C estándar. El C utiliza 7 palabras clave para definir estos tipos: int long short unsigned charfloat double
Las cuatro primeras palabras clave se utilizan para representar enteros, es decir, números sin parte decimal. Se pueden usar en solitario o formando ciertas combinaciones como unsigned short. La siguiente palabra clave, char, se utiliza para las letras del alfabeto y otros caracteres, como = , $, % y &. finalmente, las dos últimas palabras clave se usan para representar números con punto decimal. (Como es sabido, la práctica totalidad de los ordenadores utilizan punto en lugar de coma en números con decimales.) Los tipos creados con estas palabras clave se pueden dividir en dos familias, basándose en la forma de almacenamiento en el ordenador. Las cinco primeras pala bras producen tipos “enteros”, en tanto que las dos últimas generan tipos en “punto flotante”. ¿Tipos enteros? ¿Tipos de punto flotante? Si encuentra que estos términos le resultan demasiado poco familiares, no se preocupe, vamos a dar un breve repaso a los mismos a continuación. Si no está familiarizado con términos como “bits”, “bytes” o “palabras”, probablemente le conviene leer en primer lugar el recuadro siguiente. ¿Debo aprender todos los detalles? En realidad, no; de igual manera que no es necesario saber los principios de los motores de combustión interna para conducir un coche. De todas formas, un pequeño barniz de conocimientos acerca de lo que sucede en el interior de un ordenador o de un motor puede ser de gran ayuda en ocasiones. Tam bién le ayudará a ser un fascinante interlocutor.
BITS, BYTES Y PALABRAS Los términos “bit”, “byte” y “palabra” se pueden utilizar para descri bir unidades de datos en el ordenador o unidades de memoria. Aquí nos ocuparemos de la segunda acepción.
La unidad de memoria más pequeña en el ordenador se denomina bit. Puede tener únicamente dos valores: 0 ó 1 (también se puede decir que el bit está “conectado” o “desconectado”, o bien, “alto” o “bajo”; son va rias formas de indicar lo mismo). Realmente, no se puede almacenar mu cha información en un bit, pero el ordenador tiene auténtica cantidad de ellos; se puede decir que el bit es el ladrillo con el que construimos la me moria del ordenador. El byte es una unidad de memoria más útil. En la mayor parte de los ordenadores un byte se compone de 8 bits. Como cada bit puede tomar el valor 0 ó 1, hay un total de 256 combinaciones (es decir, 2 elevado a la octa va potencia) de ceros y unos formados con los bits de un byte. Con estas combinaciones, por ejemplo, podemos representar los enteros comprendi dos entre 0 y 255 o bien un conjunto de caracteres. Esta representación se puede conseguir utilizando un “código binario”, el cual emplea precisamente ceros (0) y unos (1) para representar números. Hemos incluido una discu sión sobre el código binario en el apéndice; puede leerla sin compromiso. La unidad natural de memoria para un ordenador determinado es la pa labra. Para un microordenador de “8 bits”, como los Sinclair o los Apple originales, una palabra representa exactamente un byte. Muchos sistemas más recientes, tales como el IBM PC y el Apple Lisa, son máquinas de “16 bits”. Con ello se quiere decir que el tamaño de la palabra son 16 bits, equi valente a 2 bytes. Los ordenadores más grandes pueden trabajar con pala bras de 32 bits, 64 bits o incluso más. Evidentemente, cuanto mayor sea la palabra, más información podrá almacenar. Los ordenadores suelen, en general, encadenar dos o más palabras, para poder almacenar datos de ma yor tamaño, pero este proceso hace más lenta la ejecución. Supondremos en nuestros ejemplos que se dispone de un tamaño de pa labra de 16 bits, a menos que indique lo contrario.
Para el ser humano, la diferencia entre un número entero y de punto flo tante queda establecida por la forma en que se escribe. Para el ordenador, esta diferencia se refleja en la forma en que se almacena. Veamos a conti nuación cada una de las dos clases, por orden. El entero
Un entero es un número “exacto”. Carece de parte fraccionaria y, en C, se escribe sin punto decimal. Como ejemplos podemos mencionar 2, -23 y 2456; no son enteros, sin embargo, 3.14 ó 2/3. Los enteros se almacenan de una manera muy directa como números binarios. Para almacenar el ente ro 7, por ejemplo, se escribe 111 en binario. Así, si queremos que este núme ro ocupe una palabra de un byte, simplemente hacemos que los 5 primeros bits sean 0 y los 3 últimos sean 1. Véase la figura 3.2.
51
Figura 3.2 Almacenamiento del entero 7 en código binario
Figura 3.3 Almacenamiento del número PI en punto flotante (versión decimal)
El número en punto flotante
Los números de punto flotante corresponden más o menos a lo que los matemáticos llaman “números reales”. Se incluyen en ellos los números com prendidos entre los enteros. Algunos ejemplos: 2.75, 3.16E7, 7.00, y 2e-8. Obviamente, hay más de una forma de escribir un número en punto flotante. Discutiremos más adelante la notación “E”; en síntesis, un número como “3.16E7” significa que se ha de multiplicar 3.16 por 10 elevado a la séptima potencia, es decir, un 1 seguido de 7 ceros. El 7 recibe el nombre de “expo nente”. El punto clave que hay que considerar aquí es que el esquema utilizado para almacenar un número de punto flotante es diferente del que se usa para enteros. Una representación en punto flotante implica trocear el número en una parte fraccionaria y una parte de exponente, y almacenar estas partes separadamente. Así, el 7.00 dado como ejemplo no se almacenará de la mis ma forma que el entero 7, aunque ambos tengan el mismo valor. La analogía decimal sería escribir “7.0” como “0.7E1”, siendo “0.7” la parte fraccio naria, y “1”, la parte exponencial. Por supuesto, el ordenador utilizará nú meros binarios y potencias de dos, en lugar de potencias de 10, para su alma cenamiento interno. Se puede encontrar más información de esta materia en el apéndice G. Por ahora nos concentraremos en las diferencias prácticas, que son las siguientes: 1. Los enteros son números naturales (incluyendo los negativos), en tan to que los números en punto flotante pueden representar tanto núme ros enteros como fraccionarios. 2. Los números en punto flotante pueden abarcar un rango de valores mucho mayor que el de los enteros. Véase tabla 3.1. 3. En algunas operaciones aritméticas, tales como la sustracción de nú meros muy grandes, los números en punto flotante pueden presentar grandes pérdidas de precisión. 4. Las operaciones en punto flotante son, en general, más lentas que las operaciones entre enteros. Sin embargo, existen actualmente micropro cesadores diseñados específicamente para manejar operaciones en punto flotante, que son bastante veloces.
ERRORES DE REDONDEO EN PUNTO FLOTANTE Cójase un número. Súmesele 1 y réstese del número original. ¿Qué re sultado obtenemos? Por supuesto, 1. Pero un cálculo en punto flotante puede dar una respuesta bien diferente: /* error en punto flotante */ main() {
float a,b; b = 2. 0e20 + 1. 0; a = b - 2.0e20;
}
printf ( " % f \n", a);
La salida es: o.oooooo La razón para un resultado tan llamativo es que el ordenador no es capaz de anotar suficientes cifras decimales para realizar la operación correcta mente. El número 2.0e20 es un 2 seguido de 20 ceros, y al sumarle 1 estaría mos intentando alterar el dígito 21. Para realizar la operación correctamen te, el programa debería ser capaz de almacenar un número de 21 cifras. Sin embargo, un número en punto flotante está compuesto por 6 ó 7 cifras que se gradúan a mayores o menores valores por medio del exponente; estamos condenados, por tanto, a estos errores. Por otra parte, si hubiésemos utili zado, por ejemplo, 2.0e4 en lugar de 2.0e20, habríamos obtenido la respuesta correcta, ya que aquí se trata de cambiar el quinto dígito, y los números float son lo suficientemente precisos para ello.
53
Los datos en C Revisaremos ahora en profundidad los caracteres específicos de los tipos básicos de datos utilizados en C. Para cada tipo, explicaremos cómo se declara una variable, cómo se representa una constante y cuál podría ser una aplicación típica. Algunos compiladores C no contienen todos estos tipos; consulte su manual para comprobar de qué tipos dispone en su caso. Tipos int, short y long
El C presenta una gran variedad de tipos enteros, de forma que se puede refinar un programa hasta cumplir las especificaciones de una determinada máquina o tarea. Si no desea, por el momento, complicarse la vida con estos detalles, generalmente tendrá bastante con utilizar el tipo int y olvidarse del resto de posibilidades. Los tipos de datos int, short y long son “enteros con signo” todos ellos. Dicho de otra forma, los valores permitidos para estos números son núme ros enteros positivos, o negativos, o bien cero. También existen en C “enteros sin signo”, los cuales pueden ser únicamente positivos o cero. Se utiliza un bit para indicar el signo de un número, por lo que el mayor entero con signo que se puede almacenar en una palabra será menor que el mayor entero sin signo. Por ejemplo, una palabra de 16 bits puede almacenar un entero sin signo comprendido entre 0 y 65535; esta misma palabra puede albergar cualquier entero con signo entre —32768 y + 32767. Obsérvese que el rango total es el mismo en los dos tipos.
Los diseñadores del C permitieron la opción de definir tres tamaños para enteros. El tipo int se refiere generalmente al tamaño de palabra estándar del ordenador que se está utilizando. Con respecto a los tipos short y long, se garantiza que short no es mayor que int, y que long no es menor. En algunos sistemas puede suceder que uno de estos dos tipos, o los dos, sean del mismo tamaño que int. Todo depende de la adaptación realizada en su sistema en particular. En la tabla al final de esta sección se presenta el número de bits y los diferentes tipos de datos utilizados en algunos de los ordenadores más comunes, así como el rango numérico que se puede representar en cada caso.
Declaración de tipos enteros
Simplemente teclee el tipo de variable y a continuación una lista de los nombres de las variables a utilizar. Por ejemplo: int erno; short presa; long johns; int imos, vacas, cabras;
Utilice comas para separar los nombres de las variables, y finalice la lista con un punto y coma. Las variables de un mismo tipo se pueden aunar en una sola sentencia o repartir entre varias. Por ejemplo, la sentencia de decla ración int erno, imos, vacas, cabras;
tendría el mismo efecto que las dos sentencias int del ejemplo anterior. Tam bién podría haber empleado cuatro declaraciones int por separado, una para cada variable. Se pueden utilizar combinaciones como long int o short int. Su significa do es idéntico al de los tipos long y short. Constantes enteras
Cuando se escribe un número en C sin punto decimal y sin exponente, queda clasificado como entero. Por tanto, 22 y —273 son constantes ente ras; sin embargo, 22.0 no lo es, ya que contiene un punto decimal, y 22E3, tampoco, porque está expresada en notación exponencial. Recuerde que el punto en un número se utiliza únicamente para separar la parte entera de la decimal; así pues, 23456 no es lo mismo que 23.456. Si se desea identificar una constante como de tipo long, se puede hacer colocando una L o una l al final del número. Se aconseja utilizar la L mayús cula, ya que es menos probable que se confunda con la cifra 1. Por ejemplo, una constante long sería 212L. Obviamente, el número 212 no es muy largo que digamos, pero si añade la L, se asegura que se almacenará en el número 55
adecuado de bytes. Este punto es importante cuando deseamos compatibilizar nuestro número con otras constantes o variables del tipo long. Es bastante probable que lo dicho hasta ahora sea todo lo que necesita saber acerca de constantes; comentaremos, no obstante, que el C ofrece dos opciones más. La primera, cuando un entero comienza por la cifra 0 se inter preta como un número “octal”. Se llaman octales los números que están expresados en “base 8”, es decir, que se escriben como combinaciones de po tencias de 8. Por ejemplo, el número 020 representa 2 multiplicado por la primera potencia de 8, siendo, por tanto, el equivalente octal de 16. Un simple 20, sin estar precedido por un 0, representa, sin embargo, a nuestro viejo amigo el 20. En segundo lugar, cuando se comienza un entero con 0x o 0X se interpreta como un número hexadecimal, es decir, un número en base 16. Si escribimos 0x20, indicamos 2 multiplicado por la primera potencia de 16, es decir, 32. Los números octales y hexadecimales son muy populares entre programadores. La razón es que tanto 8 como 16 son potencias de 2, y 10 no lo es; por ello, estos sistemas de numeración resultan más familiares para un ordenador. Por ejemplo, el número 65536, que surge a menudo en máquinas de 16 bits, es simplemente 10000 en hexadecimal. Si desea estudiar más profundamente este tema, le remitimos al apéndice G.
Inicialización de variables enteras
Una función usual de las constantes es la “inicialización” de variables, es decir, otorgar a una variable un valor con el que comenzar en un momento determinado de la ejecución. Ejemplos de inicialización son: erno = 1024; presa = -3; johns = 12345678;
Si lo desea, puede inicializar la variable en una sentencia de declaración. por ejemplo: int irnos = 23; int vacas = 32, cabras = 14; short perros, gatos = 92;
En la última línea se ha inicializado únicamente gatos. En principio, podría parecer que también la variable perros ha tomado el valor 92. Por ello, es aconsejable no poner en la misma sentencia de declaración variables iniinializadas y no inicializadas.
Utilización
La duda ahora es ¿qué tipo de variable entera con signo debo utilizar? Uno de los propósitos de disponer de tres tipos de diferentes tamaños es po der adaptarse a las necesidades de cada uno. Por ejemplo, si el tipo int es de una palabra de largo y long es de dos, entonces se podrán manejar núme ros mayores si se declaran como long. Si en su problema concreto no se van a utilizar números tan grandes, no use long, ya que el manejo de datos de dos palabras hace que la ejecución sea más lenta. Determinar cuándo debe utilizar int o long dependerá de su sistema, ya que una variable int en algu nos sistemas puede ser de mayor tamaño que una long de otros. En cualquier caso, como comentábamos antes, usará int la mayor parte de las veces.
“OVERFLOW” EN ENTEROS ¿Qué sucede si un entero intenta ser mayor que el máximo número asig nado a su tipo? Para comprobarlo, vamos a asignar a un entero su valor máximo y sumarle 1 sucesivamente, a fin de verificar lo que sucede. /* tehaspasado */ main() { int i = 32767; printf("%d %d %d\n", i, i + 1, i+2) ;
} cuyo resultado en nuestro sistema es: 32767 -32768 -32767
El entero i se está comportando como el velocímetro de un coche. Cuando alcanza su máximo valor vuelve a empezar desde el principio. La diferencia principal es que en un velocímetro el principio es 0, en tanto que nuestro tipo int comienza en —32768. Observe que a usted no se le avisa de que i ha sobrepasado (overflow) su valor máximo. Así pues, deberá incluir sus propias precauciones en la programación. Este comportamiento descrito aquí no forma parte de las reglas del C, pero constituye su implementación más típica.
Tipo unsigned
En general, este tipo es un modificador de alguno de los tres tipos ante riores. Podemos utilizar como tipos, por ejemplo, unsigned int o unsigned 57
long. Si usa tan sólo la palabra unsigned, se refiere implícitamente a unsigned int. El algunos sistemas no se acepta unsigned long, y se da también el caso de versiones de microprocesador en las que unsigned es un tipo separa do, con un único tamaño. Las constantes enteras sin signo se escriben de la misma forma que las constantes con signo, con la excepción obvia de que no se permite el signo menos. Asimismo, las variables enteras sin signo se declaran e inicializan de la misma forma que las demás. Por ejemplo: unsigned int alumnos; unsigned jugadores; unsigned short vicenta = 6;
Se puede utilizar este tipo para asegurar que el valor de alguna variable nunca será negativo. También, cuando se trabaja únicamente con números positivos, se puede aprovechar la ventaja de que el rango (en positivos) que se alcanza con unsigned es mayor que el tipo equivalente con signo. Una uti lización típica podría ser el acceso a direcciones de memoria o a un contador. Tipo char Este tipo define un entero sin signo en el rango 0 a 255; generalmente, dicho entero se almacena en un único byte. El ordenador utiliza un código que transforma números en caracteres, y viceversa. La mayoría de ellos usan el código ASCII, descrito en el apéndice; muchos ordenadores IBM (aunque no el IBM PC) utilizan un código diferente llamado EBCDIC. Por nuestra parte, utilizaremos en todo el libro el código ASCII, con el fin de poder dar ejemplos definidos. Declaración de variables carácter Utilizamos la palabra clave char para declarar una variable carácter. Las reglas concernientes a la declaración de más de una variable y a la inicialización de las mismas son equivalentes a las de los demás tipos básicos. Por con siguiente, todos los ejemplos que siguen son correctos: char respuesta; char latan, ismatico; char treuse = 'S' ;
Constantes carácter En C, los caracteres se definen con apóstrofos. Así, si deseamos asignar un valor a la variable char beber utilizaremos beber
= ' T'
;
/* CORRECTO */
y no beber =
T;
/* INCORRECTO */
Si se omiten los apóstrofos, el compilador pensará que estamos utilizan do una variable llamada T. También pensará que nos hemos olvidado de de clarar dicha variable. En C estándar, una constante o variable char puede representar tan sólo un único carácter. Por tanto, la siguiente secuencia no está permitida, ya que intenta asignar dos caracteres a la variable borrico. char borrico; borrico = ’ t u ’ ;
/* NO ACEPTABLE */
Si observamos una tabla de caracteres ASCII, veremos que algunos de los “caracteres” no son imprimibles. Por ejemplo, el carácter número 7 tie ne por función hacer sonar el altavoz del ordenador. Pero, ¿cómo podemos escribir un carácter que no se puede teclear? El C permite dos formas de ha cerlo. La primera de las formas es utilizar el propio código ASCII. Simplemen te, se usa el número de código ASCII precedido por un carácter barra-atrás. Ya hemos empleado esta técnica en nuestro programa el dorado en la línea: pita = ’\007’ ;
Para seguir este camino hay que tener en cuenta dos puntos: el primero es que la secuencia de código ASCII está también encerrada entre apóstro fos, como si se tratase de un carácter ordinario. El segundo es que el número de código debe escribirse en octal. Comentaremos, además, que se pueden omitir los ceros a la izquierda del código. Podríamos haber utilizado ‘ \07' o incluso ‘ \7’ para representar este carácter. Sin embargo, no se pueden omitir los ceros a la derecha; la representación ' \ 020' puede escribirse como '\ 20' pero no como '\ 02' Cuando utilice código ASCII observe la diferencia entre números y ca racteres que representan números. Por ejemplo, el carácter “4” se represen ta con un valor de código ASCII 52. Con él nos estamos refiriendo al símbo lo “4”, y no al valor numérico 4. El segundo método que utiliza el C para representar caracteres raros es usar secuencias especiales de símbolos. Se denominan “secuencias de esca pe”, y son las siguientes: \n \t \b \r \f \\ \' \"
nueva linea tabulado retroceso retorno de carro salto de pagina barra-atras(\) apostrofe (' ) com illas ( " )
59
Llegado este punto, probablemente tendrá en mente dos preguntas: 1. ¿Por qué no encerramos las secuencias de escape entre apóstrofos en el último ejem plo? 2. ¿Cuándo se debe utilizar el código ASCII y cuándo las secuencias de escape? (Esperamos que sus dos preguntas sean éstas, porque son las úni cas para las que hemos preparado respuesta.) 1. Cuando un carácter, sea secuencia de escape o no, forma parte de una cadena de caracteres encerrada entre comillas no se debe encerrar, a su vez, entre apóstrofos. Obsérvese que los demás caracteres del ejem plo (E, l, j, e, f, e, etc.) tampoco están encerrados por apóstrofos. Una cadena de caracteres encerrada entre comillas se denomina “tira de ca racteres”. Hablaremos de este tema en el siguiente capítulo. 2. Cuando pueda elegir entre el uso de una de las secuencias de escape especiales, por ejemplo ‘ \ f', y su código ASCII equivalente, por ejem plo ‘\016\ utilice la primera, ‘ \ f ' En primer lugar, la representa ción es más mnemotécnica. Además, es más transportable. Incluso si su sistema no utiliza código ASCII, la secuencia ‘ \f’ funcionará co rrectamente. Figura 3.4
Constantes de la familia int
También éstos deben encerrarse entre apóstrofos para poder ser asígna dos a una variable carácter. Por ejemplo, podemos construir la sentencia
Un programa
Presentamos aquí un corto programa que permite averiguar el número de código de un carácter en su sistema incluso si éste no trabaja en código ASCII: main() /* halla el numero de codigo de un carácter */ { char ch;
slin = ’ \n' ;
y a continuación imprimir la variable slin para avanzar la impresora o pantalla una línea. Las cinco primeras secuencias de escape son caracteres de control comunes en una impresora. El carácter nueva línea (new line) envía el cursor a la línea siguiente. El tabulado ( tab) mueve el cursor un determinado número de espacios, en general 5 u 8. El retroceso (backspace) retrocede un espacio. El carácter retorno de carro (carriage return) envía el cursor al comienzo de la línea. Salto de página avanza el papel de la impresora hasta el comienzo de la siguiente página. Las tres últimas secuencias permiten utilizar como constantes carácter los símbolos \, ‘ y “ (estos símbolos se utilizan para definir constantes carácter como parte del comando printf( ); por ello, la situación podría ser confusa si se utilizaran literalmente). Por ejemplo, si desea imprimir la línea: El jefe dice que "una \
es una barra-atras".
utilice: printf("El jefe dice que \"una \\ es una barra-atras\".\n");
printf("Introduzca un caracter. \n");
}
scanf("%c", &ch) ; /* el usuario introduce un caracter */ printf("El codigo de %c es %d. \n", ch, ch) ;
Al ejecutar el programa recuerde utilizar la tecla [enter] o [return] des pués de teclear el carácter. Así, scanf( ) captura el carácter que ha tecleado, y el símbolo & ( ampersand) se preocupa de que dicho carácter sea asignado a la variable carácter ch. A continuación, printf( ) imprime el valor de ch dos veces, la primera como carácter (indicada por el código %c) y la segunda como entero decimal (indicada por el código °7od). Tipos float y double
Los distintos tipos int se pueden utilizar para la mayor parte de proyectos de desarrollo en software. Sin embargo, los programas que implican un ma yor número de cálculos matemáticos a menudo utilizan números de “punto flotante”. En C se llama a estos números de tipo float; corresponden a los 61
tipos real de FORTRAN y PASCAL. Con este tipo de números, como ya observó anteriormente, se pueden representar rangos mucho mayores, inclu yendo fracciones decimales. Los números en punto flotante son análogos a la notación científica, sistema inventado por los científicos para expresar nú meros muy grandes y muy pequeños y para asustar a los advenedizos. Repa sémoslos someramente. En notación científica, los números se presentan como números decimales multiplicados por potencias de 10. He aquí algunos ejemplos. NUMERO NOTACION CIENTIFICA NOTACION EXPONENCIAL 1 000 000 000 = 1.0 x 109 = 1.0e9 123 000 = 1.23 x 105 = 1.23e5 322.56 = 3.2256 x 102 = 3.2256e2 0.000056 = 5.6 x 10-5 = 5.6e-5
En la primera columna hemos representado los números como se indican normalmente; en la segunda, en notación científica, y en la tercera, en la for ma en que la notación científica se escribe por y para ordenadores, con una letra “e” seguida por la potencia de 10. Generalmente, se utilizan 32 bits para almacenar un número en punto flo tante. De ellos, se usan 8 bits para expresar el valor del exponente y su signo, y 24 bits para representar la parte no exponencial. De ahí se deduce una im portante conclusión: este sistema permite una precisión de 6 ó 7 cifras decimales y un rango de ±. (10-37 hasta 10+38). Estos números, en consecuencia, re sultan muy útiles si deseamos representar la masa del Sol (2.0e30 kilogramos) o la carga de un protón (1,6e-19 culombios). (Nos encanta usar estos números.) Muchos sistemas aceptan también el tipo double (doble precisión), que utiliza el doble de bits, normalmente 64. Algunos sistemas incorporan los 32 bits adicionales a la parte no exponencial; de este modo se incrementaría el número de cifras significativas y se reducirían los errores por redondeo. Otros
sistemas usan algunos de los bits para aceptar mayores exponentes; así se in crementa el rango de números que pueden ser aceptados. Otra forma de especificar el tipo double es usar la combinación de pala bras clave long float. Declaración de variables en punto flotante
Las variables en punto flotante se declaran e inicializan de la misma for ma que sus primos los enteros. Por ejemplo: float noe, jonas; double blanca; float planck = 6.63e-34;
Constantes en punto flotante
Se nos presentan múltiples elecciones para escribir una constante en pun to flotante. La forma más general de este tipo de constantes es una serie de cifras con signo incluyendo un punto decimal, a continuación una e o E se guida de un exponente con signo que indica la potencia de 10 a utilizar. Vea mos dos ejemplos: -1.56E+12
2.87e—3
Se pueden omitir signos positivos. También se puede omitir el punto de cimal o la parte exponencial, pero no ambos a la vez. Se puede omitir la parte fraccionaria o la parte entera, pero no ambos (¡la verdad es que no quedaría mucho!). Algunos ejemplos de constantes válidas en punto flotante podrían ser: 3.14159
.2
4e16
.8E-5
100.
No utilice espacios dentro de una constante en punto flotante ERROR 1.56 E + 1 2 Las constantes en punto flotante se suponen de doble precisión. Por ejem plo, imaginemos que algo es una variable float, y que escribimos la sentencia algo = 4.0 * 2.0
Las dos constantes, 4.0 y 2.0, han sido almacenadas como double, utili zando (normalmente) 64 bits cada una. El producto (8, por si tenía dudas) se calcula usando aritmética de doble precisión y, una vez obtenida la res puesta, se recorta hasta el tamaño normal de float. Este sistema asegura la máxima precisión en los cálculos. Figura 3.5
Algunos números en punto flotante 63
ERRORES POR EXCESO Y DEFECTO EN PUNTO FLOTANTE
(OVERFLOW y UNDERFLOW) ¿Qué sucede si intentamos hacer que una variable float exceda su ran go? Por ejemplo, suponga que multiplica 10e38 por 100 (overflow) o divide 10e-37 por 1000 (underflow). El resultado depende del sistema; en nuestro sistema, cualquier número que excede los límites queda sustituido por el ma yor valor “legal” posible, mientras que cualquier valor que sobrepasa el límite por defecto queda sustituido por 0. En otros sistemas pueden apare cer mensajes de aviso o paradas, o bien le pueden ofrecer la elección entre varias respuestas. Intente averiguar cuáles son las reglas específicas que se dan en su sistema. Si no consigue encontrar la información, no dude en ejer cer un poco la vieja técnica de ensayo y error.
RESUMEN: COMO DECLARAR UNA VARIABLE SIMPLE 1. Escoja el tipo que necesite. 2. Escoja el nombre para la variable. 3. Utilice el siguiente formato en las sentencias de declaración: especificador de tipo, especificador de variable; El especificador de tipo está formado por una o más palabras clave defi niendo el tipo. Por ejemplo: int eres; unsigned short ija; 4. Se puede declarar más de una variable del mismo tipo en la misma senten cia separando los nombres de las variables por comas: char ch, init, os; 5. Se puede inicializar una variable en la sentencia de declaración: float masa = 6.0E24;
RESUMEN: TIPOS BASICOS DE DATOS Palabras clave:
Los tipos básicos de datos se inicializan utilizando las 7 palabras clave siguien tes: int, long, short, unsigned, char, float, double. Enteros con signo: Pueden tener valores positivos o negativos, int: es el tipo entero básico de un sistema dado. long o long int: permite almacenar un entero al menos del tamaño de int, y posiblemente mayor. short o short int: queda garantizado que el mayor entero short no es mayor que el mayor int, y puede que sea menor. Normalmente, el tipo long será ma yor que short, y el tipo int será el mismo que uno de ellos. Por ejemplo, en el IBM PC Lattice C se dispone de 16 bits para short y para int, y 32 bits para long. Todos estos datos dependen del sistema. Enteros sin signo: Pueden tomar valores positivos o cero únicamente. Se extiende el rango máxi mo de valores positivos alcanzables. Utilice la palabra clave insigned antes del tipo deseado: unsigned int, unsigned long, unsigned short. Un tipo unsigned sin nada detrás se supone unsigned int. Caracteres: Son los símbolos tipográficos tales como A, &, +. Generalmente se almace nan en un byte de memoria, char: es la palabra clave para definir este tipo. Punto flotante: Pueden tomar valores positivos o negativos, float: es el tamaño básico de punto flotante para su sistema, double o long float: es una unidad (posiblemente) mayor que permite almace nar números en punto flotante. Probablemente mantendrá un mayor número de cifras significativas, y quizá mayores exponentes.
65
Otros tipos Con esto se acaba nuestra lista de tipos fundamentales de datos. A algu nos de ustedes les parecerán un montón; otros, por el contrario, estarán pen sando que no son suficientes. ¿Qué hay de un tipo booleano o un tipo string? Bien, el C carece de ellos, pero aun así se las arregla bastante bien con mani pulaciones lógicas y con tiras de caracteres (string). Precisamente en el próxi mo capítulo hablaremos de estas últimas. Lo que sí tiene el C son otros tipos derivados de los tipos básicos. Entre estos tipos se incluyen arrays, punteros, estructuras y uniones. Aunque to dos ellos serán tratados en capítulos posteriores, comentaremos que ya he mos estado trabajando con punteros en los ejemplos de este capítulo. (Se usan punteros en la función scanf( ), indicándose en ese caso con el prefijo &.) Tamaños de los tipos Damos a continuación una tabla de los tamaños de tipos en algunos en tornos comunes de C.
Hemos averiguado el tamaño de cuatro de los tipos, pero se puede modi ficar fácilmente este programa para buscar el tamaño de cualquier otro tipo que le interese.
Utilización de tipos de datos Cuando intente desarrollar un programa, tome nota de las variables que necesita y del tipo que deben ser. La mayor parte de las veces utilizará int o posiblemente float para los números y char para los caracteres. Declárelos al comienzo de la función que los utilice. Escoja para la variable un nombre que recuerde su significado. Cuídese de ajustar los tipos al inicializar las va riables; utilice siempre el mismo tipo para la variable y su constante corres pondiente. int manzanas = 3; int naranjas = 3.0;
/* CORRECTO */ /* INCORRECTO */
Tabla 3-1. Tamaños reales de tipos en algunos sistemas
El C es más permisivo en este tipo de errores que otros lenguajes, como el PASCAL; de todas formas es conveniente no desarrollar malos hábitos desde el principio.
Hasta ahora hemos aprendido Hemos dado un buen salto con este capítulo. En el resumen nos limitare mos a los aspectos más prácticos de lo que hemos estudiado. Como en el ca pítulo anterior, incluimos pequeños ejemplos cuando lo permite el espacio. He aquí algunas de las cosas que ya debe conocer: ¿A cuál de ellos se parece su sistema? Ejecute el siguiente programa para averiguarlo. main()
{
}
printf("El tipo int ocupa %d bytes. \n", sizeof (int) ) ; printf("El tipo char ocupa %d bytes.\n", sizeof (char) ) ; printf("El tipo long ocupa %d bytes.\n", sizeof (long) ) ; printf("El tipo double ocupa %d bytes. \n", sizeof(double));
El lenguaje C incluye un operador llamado sizeof que sirve para calcular el tamaño de las cosas en bytes. La salida de este programa en nuestro siste ma es El El El El
tipo tipo tipo tipo
int ocupa 2 bytes. char ocupa 1 bytes. long ocupa 4 bytes. double ocupa 8 bytes.
Cuáles son los tipos básicos de datos en C: int, short, long, unsigned, char, float, double Cómo se declara una variable de cualquier tipo: int contador; float dine ro; etc. Cómo escribir una constante int: 256, 023, 0XF5, etc. Cómo escribir una constante char: ‘r’, ‘U', ‘\007', ‘?'. etc. Cómo escribir una constante float: 14.92, 1.67e—27, etc. Qué son palabras, bytes y bits. Cuándo utilizar los diferentes tipos de datos.
Cuestiones y respuestas Pensar un poco estas cuestiones le ayudará a digerir el material de este capítulo. 67
Cuestiones
1. ¿Qué tipo de dato utilizaría para expresar las siguientes cantidades? a. La población de Río Frito b. El peso medio de una pintura de Rembrandt c. El tipo de letra más común en este capítulo d. El número de veces que aparece esta letra 2. Identifique el tipo y significado de las siguientes constantes: a. ’ \b’ b. 1066 c.
99. 44
d. OXAA e. 2. 0e30 3. La señorita Violina Armonio Xilofón ha pergeñado un programa repleto de erro res. Ayúdela a encontrarlos. #include main
(
float g; h; float tasa, precio; g = e21; tasa = precio*g; )
Respuestas 1. a. int, posiblemente short; la población es un número entero b. float; es raro que el promedio sea un entero exacto c. char d. int; posiblemente, unsigned. 2. a. char, el carácter retroceso (backspace) b. int histórico c. float; medida de la pureza de un jabón, por ejemplo. d. int hexadecimal; su valor decimal es 170 e. float; masa del Sol en kg. 3. Línea 1: correcta Línea 2: debe haber un par de paréntesis a continuación de main, main( ) Línea 3: utilice {, no ( Línea 4: debe haber una coma entre g y h en lugar de un punto y coma Línea 5: correcta Línea 6: correcta (la línea en blanco) Línea 7: debe haber, al menos, una cifra delante de la e. Tanto le21 como 1.0e21 podrían valer. Línea 8: ok Línea 9: utilice }, no ) Faltan las líneas: en primer lugar, no se asigna ningún valor a precio. La variable h no se utiliza. Además, el programa no informa del resultado de su cálculo. Ninguno de estos errores detendrá la ejecución del programa (aunque probablemente le aparecerá un aviso por la variable sin utilizar), pero eliminan la poca utilidad que tenía.
4 Tiras de caracteres, #define, printf( ) y scan( ) En este capítulo encontrará: • Introducción a las tiras de caracteres • Longitud de tira—strlen( ) • Constantes: el preprocesador C • El C como maestro del disfraz: creación de sosias • Usos y utilidades de printf( ) y scanf( ) • Utilización de printf( ) • Modificadores de especificaciones de conversión en printf( ) • Ejemplos • Utilización de printf( ) para efectuar conversiones • Uso de scanf( ) • Claves de utilización • Hasta ahora hemos aprendido • Cuestiones y respuestas
71
sitio = sizeof nombre; letras = strlen(nombre); volumen = peso/DENSIDAD; printf("Bien, %s, tu volumen es %2.2f litros. \n", nombre, volumen); printf("Ademas, tu nombre tiene %d letras, \n", letras); printf("y disponemos de %d bytes para guardarlo.\n", sitio) ;
Tiras de caracteres, #define, printf( ) y scanf( )
}
La ejecución de secretos produce resultados como el siguiente: Hola!, como te llamas? Angelica Angelica, cual es tu peso en kg?
CONCEPTOS
62. 5
Tiras de caracteres Preprocesador C Salida con un formato
Bien, Angelica, tu volumen es 60.63 litros. Ademas, tu nombre tiene 8 letras, y disponemos de 40 bytes para guardarlo.
Seguidamente comentamos las principales novedades de este programa.
En este capítulo continuaremos el baile de datos profundizando en temas que van más allá de los tipos básicos; trataremos concretamente las tiras de caracteres. Antes de ello repasaremos una importante ayuda del C, el precesador, y aprenderemos el modo de definir y utilizar constantes simbólicas. A continuación, volveremos de nuevo a la cuestión ya mencionada de comunicación de datos entre usted y el programa; esta vez nos detendremos en las características de printf( ) y scanf( ). Seguro que a estas alturas ya esperando el programa de comienzo de lección, de manera que no vamos a decepcionarle. /* secretos */ #define DENSIDAD 0.97 /* d e n s i d a d d e l h o m b r e e n main() /* programa i n f o r m a t i v o t o t a l m e n t e i n ú t i l */
1. Hemos utilizado un “array” para guardar una “tira de caracteres”; en este caso, el nombre de una persona. 2. Hemos usado la “especificación de conversión” %s para manejar la entrada y salida de la tira. 3. Hemos utilizado el preprocesador C para definir la constante simbóli ca DENSIDAD. 4. Hemos empleado la función C strlen( ) para averiguar la longitud de la tira.
Este modo de funcionamiento del C puede parecer un poco complicado si se compara con los modos de entrada/salida de BASIC, por ejemplo. Sin embargo, con esta complejidad se pretende tener un mayor control sobre la E/S y ganar en eficiencia de ejecución; por otra parte, tampoco es tan difícil una vez que se ha repasado un par de veces. Ahondemos ahora un poco más en estas nuevas ideas.
kg por litro */
{ float peso, volumen; int sitio, letras; char nombre[40] ; /* o b i e n p r u e b e " s t a t i c c h a r nombre[40] */ printf("Hola!, como te llamas?\n"); scanf("%s", nombre) ; printf("%s, cual es tu peso en kg?\n", n o m b r e ) ; scanf("%f", &peso) ;
Introducción a las tiras de caracteres Una “tira de caracteres” (string) consiste simplemente en una serie de uno o más caracteres. Un ejemplo podría ser: "Casi me tira el viento
que soplaba"
73
Las comillas no forman parte de la tira. Sirven para especificar el comienzo y final de ésta, al igual que los apóstrofos marcaban los caracteres individuales. En C no existe un tipo especial de variable para tiras. En su lugar se al macenan como un array de tipo char. Ello permite imaginar los caracteres de la tira almacenados en células de memoria adyacentes, a razón de un ca racter por célula.
Figura 4.1
Una tira en un array
Obsérvese en la figura el carácter \0 que ocupa la última posición del array; éste se llama “carácter nulo’’, y es utilizado por el C para marcar el final de la tira de caracteres. El carácter nulo no es la cifra cero, sino un caracter no imprimible, cuyo número de código ASCII es 0. La existencia de este carácter nulo significa que el array deberá disponer de al menos una célula más del número de caracteres que vayamos a almacenar. ¿Pero qué es un array? Array es un palabra inglesa cuyo significado literal es formación, disposición ordenada. En la jerga informática se utiliza esta palabra como secuencia ordenada de datos de un determinado tipo. Por amplificar, también se puede considerar como una serie de células de memoria en fila. En nuestro ejemplo hemos creado un array compuesto de 40 células de memoria, cada una de las cuales puede almacenar un valor de tipo char. la sentencia de declaración correspondiente es: char nombre[40];
Los corchetes identifican la variable nombre como un array, 40, por su parte, indica el número de elementos, y, finalmente, char identifica el tipo cada uno de ellos. Uniones simbólicas
La sentencia char nombre [3] "encadena" tres datos de tipo char
Figura 4.2
Declaración de un nombre de array de tipo char
Por cierto, habrá observado que uno de los comentarios del programa le indica que podía utilizar alternativamente una declaración más elaborada: static char nombre[40];
Debido a las peculiares características de la función scanf( ) de nuestro sistema, tenemos que usar esta segunda forma; sin embargo, lo más proba ble es que usted no tenga que hacerlo. Si observa que la primera forma le da problemas, utilice la segunda; de hecho, la segunda forma funciona en todos los sistemas, pero no hablaremos de static hasta que discutamos los modos de almacenamiento en el capítulo 10. Esto está empezando a ponerse complicado; tenemos que crear un array, empaquetar en él los caracteres de la tira uno a uno y recordar añadir un \0 al final. Por fortuna, el ordenador se preocupa por sí mismo de la mayor parte de estos detalles. Ejecute el siguiente programa, y verá qué fácilmente se funciona en realidad /* elogio1 */ #define ELOGIO "!Por Jupiter, que gran nombre!" main()
{ char nombre[50];
}
printf("Como te llamas?\n"); scanf ("%s", nombre); printf("Hola, %s. %s\n", nombre, ELOGIO);
El símbolo %s indica a printf( ) que imprima una tira (string). La ejecu ción de elogio1 debe producir una salida similar a ésta: Como te llamas? Gonzalo Gonzalez de la Gonzalera Hola, Gonzalo. !Por Júpiter, que gran nombre!
No tenemos que poner el carácter nulo; dicha tarea la realiza scanf( ) cuan do lee la entrada. ELOGIO es una "constante de tira de caracteres”. Pronto llegaremos a la sentencia #define; por el momento, lo único que nos interesa es que las comillas que encierran la frase a continuación de ELOGIO identi fican a éste como tira, y ya se encarga el ordenador de colocarle su corres pondiente carácter nulo. Obsérvese (y esto es importante) que scanf( ) ha leído únicamente el pri mer nombre de Gonzalo González. Al realizar la lectura de entrada, scanf( ) se detiene en el primer “espacio libre” (blanco, tabulado o nueva línea) que encuentra. En nuestro ejemplo termina su busca en el espacio en blanco que hay entre “Gonzalo” y “González”. En general, scanf( ) lee palabras sim ples, no frases completas. Disponemos en C de otras funciones de lectura, 75
como gets( ), que nos permitirán manejar tiras de caracteres cualesquiera. Volveremos al tema en capítulos posteriores. Otra observación que conviene anotar es que la tira “x” no es lo mismo que el carácter ‘x’. Una diferencia entre ambos podría ser que ‘x’ pertenece a un tipo básico (char), mientras que “x” es de un tipo derivado, un array de char. La segunda diferencia es que, en realidad, “x” contiene dos carac teres, a saber, ‘x’ y el carácter nulo.
Como te llamas? Apolo Hola, Apolo. !por Júpiter, que gran nombre! Tu nombre de 5 letras ocupa 50 células de memoria. La frase de elogio tiene 30 letras y ocupa 31 células de memoria.
Ahora puede imaginar lo que ha sucedido. El array nombre tiene 50 célu las de memoria, valor que nos facilita sizeof. Sólo se han utilizado las cinco primeras para almacenar Apolo, valor del que nos informa strlen( ). La sex ta celdilla del array nombre contiene un carácter nulo, cuya presencia indica a strlen( ) que deje de contar.
'X' como cáracter 5 caracteres
45 caracteres nulos
"X" como tira
la tira acaba con un carácter nulo Figura 4.3
‘x’ y “x” Figura 4.4
strlen( ) sabe cuándo parar
Longitud de tira— strlen( ) En el último capítulo presentamos en sociedad el operador sizeof, que nos comunicaba el tamaño de los diferentes tipos en bytes. La función strlen( ) nos informa de la longitud de una tira en caracteres. Como cada carácter ne cesita un byte para su almacenamiento, uno podría pensar que vamos a obtener el mismo resultado con ambos operadores; no es así, sin embargo. Añada mos unas cuantas líneas a nuestro ejemplo anterior, y veamos por qué: /* elogio2 */ #define ELOGIO "!Por Jupiter, que gran nombre!" main() { char nombre[50]; printf("Como te llamas?\n"); scanf("%s", nombre); printf("Hola, %s. %s\n", nombre, ELOGIO); printf("Tu nombre de %d letras ocupa %d celulas de memoria. \n", strlen(nombre), sizeof nombre); printf("La frase de elogio tiene %d letras ", strlen(ELOGIO)); printf ("y ocupa %d celulas de memoria. \n", sizeof ELOGIO); }
Por cierto, observe que hemos usado dos métodos distintos para manejar sentencias printf( ) largas. Hemos repartido una sentencia en dos líneas; po demos partir la línea entre argumentos, pero no en mitad del texto entreco millado. En el otro caso hemos empleado dos sentencias printf( ) para im primir una misma línea. La ejecución de este programa podría dar un resultado:
Cuando llegamos a ELOGIO, observamos que strlen( ) nos da de nuevo número exacto de caracteres (incluyendo espacios y signos de puntuación) de la tira. El operador sizeof nos responde con un número mayor en una uni dad, ya que también está contando el carácter nulo invisible para terminar la tira. Observará que no tenemos que indicar al ordenador cuánta memoria debe reservar para almacenar la frase; la tarea se realiza automáticamente contando el número de caracteres comprendido entre las comillas. Un detalle más: en el capítulo anterior hemos empleado sizeof con parén tesis, y en este capítulo no. La cuestión de si se deben o no utilizar paréntesis depende de si desea saber el tamaño de un determinado tipo o el de una can tidad en concreto. Es decir, se utilizará sizeof(char) o sizeof(float), pero en cambio se usará sizeof nombre o sizeof 6.28. Aquí se ha utilizado sizeof y strlen( ) para satisfacer nuestra curiosidad; sin embargo, estos operadores son mucho más valiosos que todo eso. strlen( ), por ejemplo, es muy útil en todo tipo de programas de tiras de caracteres, como veremos en el capítulo 13. Nos ocuparemos ahora de la sentencia #define.
Constantes: el preprocesador C En ocasiones se necesita utilizar una constante dentro de un programa. Por ejemplo, se puede calcular la longitud de una circunferencia como circ = 3. 14 * diámetro; 77
Hemos usado aquí la constante 3.14 para representar al famoso número pi. Si queremos emplear una constante, podemos simplemente teclear su va lor real, como hemos hecho aquí; no obstante, hay buenas razones para sus tituir el número por una “constante simbólica”, es decir, utilizar una senten cia así: circ = pi * diametro ;
y dejar que el ordenador sustituya el símbolo por su valor real más adelante. ¿Por qué es aconsejable esta práctica? En primer lugar, un nombre da más información que un número. Compárense las dos sentencias siguientes: sepaga = 0.015 * valor; sepaga = tasa * valor;
Cuando esté escribiendo un programa largo, encontrará que la segunda sentencia se reconoce con más facilidad. En segundo lugar, si definimos la constante al comienzo del programa, podremos modificarla con toda facilidad. Después de todo, los impuestos, ¡ay!, varían, e incluso una constante tan libre de toda sospecha como pi fue en una ocasión establecida como 3 1/7 por ley en un determinado Estado de los Estados Unidos. (Probablemente, más de un círculo se encontró de pronto fuera de la ley y fugitivo de la justicia.) De esta forma, cambiar el valor de la constante supone la modificación de una sola sentencia; si, por el contrario, utilizamos el número, habremos de localizarlo a lo largo de to do el programa. De acuerdo, me ha convencido. ¿Cómo se establece una constante sim bólica? Una forma de hacerlo es declarar una variable e igualarla a la cons tante deseada, es decir: float tasa; tasa = 0.015;
Este sistema puede ser válido para un programa pequeño, pero resulta despilfarrador de tiempo, ya que el ordenador tiene que buscar el valor de tasa en la dirección de memoria que le corresponda cada vez que se utiliza. Este podría ser un ejemplo de sustitución en tiempo de ejecución, ya que las sustituciones se realizan mientras el programa se ejecuta. Afortunadamente, en C se les ocurrió una idea mejor. La idea mejor es el preprocesador C. Ya hemos visto en el capítulo 2 có mo utiliza el preprocesador #include para incluir información de otro fiche ro. También permite definir constantes; simplemente añada una línea al co mienzo de su programa, como la siguiente: #define TASA 0.015
Al compilar el programa, el valor 0.015 será sustituido en cualquier lugar que aparezca TASA. Este proceso se denomina sustitución en “tiempo de compilación”. Cuando se ejecute el programa, ya estarán hechas todas las sustituciones pertinentes. Observe con atención el formato. En primer lugar aparece #define. Debe estar colocado completamente a la izquierda. A continuación se indica el nom bre simbólico de la constante y el valor de la misma. No se usan símbolos, como punto y coma, ya que no se trata de una sentencia C. ¿Por qué se escribe TASA con mayúsculas? Bien, es simplemente una tradición en C escribir estas constantes con letras mayúsculas. Así, cuando se interne en las profundida des de un programa podrá saber instantáneamente si un nombre determinado corresponde a una variable o a una constante; es, por tanto, otro ejemplo de nuestro empeño en hacer más legibles los programas. Por supuesto que el programa funcionará también si coloca las constantes en letras minúscu las, pero estamos seguros de que, después de lo dicho, se sentirá un poco cul pable si lo hace. Veamos a continuación un ejemplo sencillo: /* pizza */ #define PI 3.14153 main() /* aprendamos los misterios de la pizza */ {
float area, circun, radio;
}
printf("Cual es el radio de su pizza?\n"); scanf ( "%f", &radio) ; area = PI * radio * radio; circun = 2 . 0 * PI * radio; printf("Los parametros basicos de su pizza son:\n"); printf("circunferencia = %1.2f, area = %1.2f\n", circun, area);
La cadena de símbolos %1.2f de la sentencia printf( ) hace que la salida quede redondeada a dos cifras decimales. Evidentemente, el programa no sirve para demostrar las propiedades más importantes de las pizzas, especialmente las organolépticas, pero contribuye a iluminar una pequeña parte del miste rioso mundo de los programas de pizzas. Un ejemplo de salida de este programa podría ser: Cual es el radio de su pizza?
6.0 Los parametros básicos de su pizza son: circunferencia = 37.70, area = 113.10
La sentencia #define se puede utilizar también para constantes de tipo carácter y tiras de caracteres. Simplemente se han de emplear apóstrofos pa ra las primeras y comillas para las segundas. Así, son ejemplos válidos: 79
que debe ir al comienzo del programa. El preprocesador, por su parte, igno ra si se ha usado .h o no. El C como maestro del disfraz: creación de sosias
Las habilidades de #define van más allá de la representación simbólica de constantes. Consideremos, sin ir más lejos, el programa siguiente: #include "jerga.h" programa begin entero tuyo, mio vale suelta("Escribe un entero\n") vale traga("%d", &tuyo) vale mio = tuyo por DOS vale suelta("%d es el doble de tu numero!", mio) vale end (aquí interviene el preprocesador)
¡Caramba! esto parece vagamente familiar, recuerda al PASCAL; pero seguro que no es C. El secreto, por supuesto, está en el fichero jerga.h. ¿Qué contiene? Veamos: jerg a.h #d efine #d efine #d efine #d efine #d efine #d efine #d efine #d efine #d efine
Figura 4.5
Lo que usted teclea y lo que se compila #define PITA '\007' #define ESS 'S' #define NULO ' \0' #define GANAR "Lo lograste, forastero!"
Dediquémonos ahora a caza mayor. Supongamos que desarrolla un pa quete completo de programas que utilizan el mismo conjunto de constantes. Le conviene hacer lo siguiente (especialmente si es perezoso): 1. Reunir todas sus sentencias #define en un fichero, llamándolo, por ejem plo, const.h. 2. En el encabezado de cada uno de los programas incluir la sentencia #include “const.h”. Así, cuando ejecute uno de los programas, el preprocesador leerá el fiche ro const.h y utilizará todas las sentencias que allí se encuentren. Por cierto, el .h del final del nombre del fichero es un recordatorio de que el propio fi chero actúa de “encabezamiento” ( header), es decir, una cierta información
p r o g r a m a m a in ( ) begin { end } vale ; traga scanf suelta printf DOS 2 por * ente ro int
Este ejemplo demuestra cómo funciona el preprocesador. Se localizan en su programa las palabras definidas en sentencias #define, y se sustituyen lite ralmente por su equivalente. En nuestro ejemplo, todos los vales son trans formados en puntos y coma, etc., antes de proceder a la compilación. El pro grama resultante es idéntico al que hubiese obtenido utilizando las palabras habituales de C. Esta potencialidad se puede usar para definir un “macro”; volveremos sobre el tema en el capítulo 11. Existen algunas limitaciones. Así, las partes de programa encerradas en tre comillas no son sustituibles. Por ejemplo, la siguiente combinación no funciona: #define MN "minimidimaximalismo" printf("Creia profundamente en el MN.\n");
La salida será Creia profundamente en el MN.
Sin embargo, la sentencia: printf ( "Creia profundamente en el %s. \n", MN) ; 81
Utilización de printf( )
dará como resultado Creia profundamente en el minimidimaximalismo
En el segundo caso, MN está fuera de la zona entrecomillada, siendo, por tanto, sustituible por su valor. En resumen, el preprocesador C es una herramienta útil y de gran ayuda, por lo que aconsejamos que se emplee cuando sea posible. Iremos mostran do más aplicaciones del mismo según avancemos.
El programa siguiente emplea algunos de los identificadores que acaba mos de ver: /* imprimecosas */ #define PI 3. 14159
main ()
{
int numero = 5; float ron = 13.5; int coste = 31000;
Utilidades de printf( ) y scanf( ) Las funciones printf( ) y scanf( ) permiten al programa comunicarse con el exterior. Se denominan funciones de entrada/salida o funciones E/S, para abreviar. No son las únicas funciones E/S que hay en C, pero sí las más ver sátiles. Anotemos que estas funciones no forman parte de la definición del C; de hecho, en C se deja la implementación de E/S a los diseñadores del com pilador: así se consigue optimizar las funciones para cada máquina específi ca. Por otra parte, distintos sistemas han intentado compatibilizarse entre sí, ofreciendo versiones de scanf( ) y printf( ). Así pues, lo que se dice aquí es cierto para la mayoría de los sistemas, pero si no funciona en el suyo en concreto no se deje llevar por el pánico. Generalmente, printf( ) y scanf( ) funcionan de la misma forma, utili zando cada una de ellas una “tira de caracteres de control” y una lista de “argumentos”. Estudiaremos estas características; en primer lugar en printf( ), y a continuación en scanf( ). Las instrucciones que se han de dar a printf( ) cuando se desea imprimir una variable dependen del tipo de variable de que se trate. Así, tendremos que utilizar la notación %d para imprimir un entero, y %c para imprimir un carácter, como ya se ha hecho. A continuación damos la lista de todos los identificadores que emplea la función printf( ), e inmediatamente mos traremos cómo usarlos. En la siguiente tabla se muestran dichos identifica dores y el tipo de salida que imprimen. La mayor parte de sus necesidades queda cubierta con los cinco primeros; de todas formas, ahí están los cuatro restantes por si desea emplearlos. IDENTIFICADOR
%d %c %s %e %f %g %u %o %x
}
printf(”Las %d mujeres se bebieron %f vasos de ron. \n", n u m e r o , ron) ; printf("El valor de pi es %f. \n", P I ) ; p r i n t f ( " F a z e r non quiso q u e t a l m a l a n d r i n f a b l a r a . \n") ; p r i n t f ( "%c%d\n", ' $ ’ , c o s t e ) ;
Evidentemente, la salida es: La s 5 m u je res se beb ieron 13 .500 000 vaso s de ron. El valor de pi es 3.14159. Fazer non quiso que tal malandrin fablara. $31000
El formato para uso de printf( ) es éste: p r i n t f ( C o n t r o l , i t e m 1 , i t e m 2 , ........................... ) ;
Item1, item2, etc., son las distintas variables o constantes a imprimir. Pue den también ser expresiones, las cuales se evalúan antes de imprimir el resul tado. Control es una tira de caracteres que describe la manera en que han de imprimirse los items. Por ejemplo, en la sentencia printf(”Las %d mujeres se bebieron %f vasos de ron. \n", numero, ron);
SALIDA
Entero decimal Caracter Tira de caracteres Número de punto flotante en notación exponencial Número de punto flotante en notación decimal Use %f o %e, el que sea más corto Entero decimal sin signo Entero octal sin signo Entero hexadecimal sin signo
Veamos ahora cómo se utilizan.
control sería la frase entre comillas (después de todo, es una tira de caracte res), y número y ron serían los items; en este caso, los valores de dos varia bles.
Veamos otro ejemplo: printf("El valor de pi es %f. \n", P I ) ;
En esta ocasión, la lista del final tiene un único miembro, la constante simbólica PI. 83
Observe que en el segundo ejemplo el primer ítem de la lista a imprimir era una constante carácter, no una variable. Suponemos que ya se habrá percatado de un pequeño problema. Al utili zar la tira de control el símbolo % para identificar los especificadores de con versión pueden aparecer complicaciones si se pretende imprimir el propio % como símbolo; si se usa en solitario, el compilador lo tomará como especificador, y se formará un pequeño lío. El sistema para imprimirlo es simple, basta con emplear el símbolo % duplicado. Así:
S entencia
Figura 4.6
Argumentos en printf ( )
Observamos que la parte de control contiene dos clases distintas de infor mación: 1. Caracteres que se han de imprimir tal como están. 2. Identificadores de datos, también llamados “especificaciones de con versión”.
pc = 2*6; printf ("Un %d%% del beneficio de Simplicio era ficticio. \n", pc) ;
produce la salida siguiente: Un 12% del beneficio de Simplicio era ficticio.
limitada por comillas
Modificadores de especificaciones de conversión en printf( )
caracteres literales
caracteres literales
especificación de conversión
Figura 4.7
Anatomía de una tira de control
Debe existir una especificación de conversión por cada ítem que aparezca en la lista que sigue a la tira de control. ¡Ay de aquel que olvide este manda miento básico! Recibirá el justo castigo a su perversidad o su despiste. Una cosa como printf ("El resultado fue Calamares %d, Jibias %d.\n", tanteo1);
no tiene valor asignado al segundo %d. El resultado concreto depende de su sistema, pero le aseguramos que en el mejor de los casos obtendrá datos sin sentido. Cuando desee escribir simplemente una frase no necesita especificadores. Por el contrario, si quiere imprimir tan sólo datos, puede ahorrarse la frase inicial. Por ello las dos sentencias siguientes son válidas: printf("Fazer non quiso que tal malandrin fablara.\n"); printf("%c%d\n",'$', coste);
Los modificadores son apéndices que se agregan a los especificadores de conversión básicos para modificar (¿qué otra cosa iba a ser?) la salida. Se colocan entre el símbolo % y el carácter que define el tipo de conversión. A continuación se da una lista de los símbolos que está permitido emplear. Si se utiliza más de un modificador en el mismo sitio, el orden en que se indi can deberá ser el mismo que aparece en la tabla. Tenga presente que no todas las combinaciones son posibles. Modificador Significado — El ítem correspondiente se comenzará a escribir empezando en el extremo izquierdo del campo que tenga asignado (véase abajo). Normalmente se escribe el ítem de forma que acabe a la derecha del campo. Ejemplo: %-10d
número Anchura mínima del campo. En el caso de que la cantidad a imprimir (o la tira de caracteres) no quepa en el lugar asigna do, se usará automáticamente un campo mayor. Ejemplo: %4d número Precisión. En tipos flotantes es la cantidad de cifras que se han de imprimir a la derecha del punto (es decir, el número de decimales). En el caso de tiras, es el máximo número de caracteres que se ha de imprimir. Ejemplo: %4.2f (dos decimales en un campo de cuatro carac teres de ancho). 1 El dato correspondiente es de tipo long en vez de int. Ejemplo: %ld
Ejemplos Vamos a hacer que funcionen estos modificadores. Comenzaremos por observar el efecto que produce el modificador de anchura de campo en la impresión de un entero. Considérese el siguiente programa: {
printf("/%d/\n", 336) ; printf("/%2d/\n", 336); printf("/%l0d/\n", 336) ; printf("/%-10d/\n", 336) ;
Este programa imprime la misma cantidad cuatro veces usando 4 especi ficaciones de conversión diferentes. Hemos colocado un símbolo / al comienzo y al final para que se pueda observar dónde empieza y termina cada campo. La salida del programa es la siguiente: /336 / /336 / / / 336
1234.56) ; 1234.56); 1234.56); 1234.56);
cuya salida es:
main()
}
}
printf(/%4.2f/\n", printf("/ %3. 1f/\n", printf("/%l0.3f/\n", printf("/%10.3e/\n",
336 /
/
La primera especificación de conversión es %d, sin modificadores. Ob servamos que el campo asignado tiene la misma anchura que el entero a im primir. Esta es la llamada “opción por defecto”, es decir, lo que hace la má quina cuando no se le indica otra cosa. La segunda especificación de conversión empleada es %2d. Este especificador debería producir un campo de dos espacios de ancho, pero, al ser ma yor el entero a imprimir, el campo se expande hasta el tamaño del mismo. Así se evita que el número quede “recortado”. El siguiente especificador es %10d Con él se consigue un campo de 10 espacios, que, en efecto, podemos ver reflejado a la salida: hay 7 espacios en blanco y 3 dígitos entre las marcas colocadas para delimitación. Observe que el número impreso se ajusta al margen derecho de su campo. Finalmente, la última especificación empleada es %-10d. También con ella se obtiene un campo de 10 espacios, pero esta vez el número queda justi ficado a la izquierda debido al signo —, como ya se indicó. Una vez que domine el tema, comprobará que los especificadores y mo dificadores que acompañan al C permiten un excelente control sobre el as pecto de la salida de datos en sus programas. Vayamos ahora con los formatos en un punto flotante. Como primera providencia, prepararemos un programa como el siguiente:
/1234.560059/ /1.234560E+03/ /123A.56/ / 1234.560/ / 1.234E+03/
De nuevo comenzamos con la opción por defecto, que en este caso es %f. En los números en punto flotante aparecen dos opciones por defecto: la anchura del campo y el número de decimales. Ninguno de ellos ha sido espe cificado, por lo que el ordenador lo hace por su cuenta. La segunda opción por defecto son 6 decimales, en tanto que la primera es el campo mínimo en que quepa el número completo. Observe que el número impreso es ligera mente diferente del original con el que se empezó. Ello es debido a que esta mos imprimiendo un total de 10 cifras, mientras que los números en punto flotante de nuestro sistema tienen una precisión de 6 ó 7 cifras a lo sumo.
main()
{
printf("/%f/\n”, 1234. 56) ; printf ("/%e/\n", 1234. 56) ;
87
El especificador siguiente es una opción por defecto %e. Como se puede comprobar, escribe un número a la izquierda del punto decimal y seis a la derecha. En cualquiera de los casos, parece que estamos obteniendo dema siados dígitos. El remedio es especificar el número de decimales a escribir a la derecha del punto, opción empleada en los cuatro últimos casos. Observe que en el cuarto y sexto caso se produce un redondeo de nuestro número original. Pasemos ahora a estudiar los especificadores para tiras de caracteres. De diquemos nuestra atención al ejemplo siguiente:
En todo caso, seremos razonables en nuestras ambiciones, y nos restringire mos a la familia de tipos enteros. Utilización de printf( ) para efectuar conversiones
De nuevo vamos a imprimir enteros. Como ya hemos aprendido a mane jarnos por el ancho del campo, esta vez no nos molestaremos en usar / como marca para determinarlo. main()
#define PINTA "Emocionante accion!"
{
main() {
}
printf("%2s/\n", PINTA) ; printf("%22s/\n", PINTA); printf("%22.5s/\n", PINTA); printf("%-22.5s/\n", PINTA) ;
que da como salida:
El resultado de la ejecución de este programa en nuestro sistema es el si guiente:
/Emocionante accion!/ / Emocionante accion!/
/ /Emoci
}
pri nt f( "%d\ n", 336) ; pri nt f ( "%o\ n", 336) ; pri nt f ( "%x\ n", 336) ; pri nt f ( "%d\ n", - 336) ; pri nt f ( "%u\ n", -336) ;
Emoci/ /
Nótese cómo se expande el campo para dar cabida a todos los caracteres especificados. Obsérvese también que el número de la derecha del punto deci mal, que actuaba como modificador de precisión, es ahora un indicador del número de caracteres a imprimir; así, el .5 incluido en el formato indica a printf( ) que imprima tan sólo 5 caracteres. Ya hemos visto algunos ejemplos. ¿Sería ahora capaz de preparar un de terminado formato que imprimiese algo con la forma siguiente? La familia NOMBRE debe tener XXX.XX millones!
En este ejemplo NOMBRE y XXX.XX representan valores a ser suminis trados por el programa, a partir de dos variables que llamaremos nombre[40] y dinero. Una posible solución podría ser: printf("La familia %s debe tener %.2f millones! \n", nombre,. dinero) ;
Hasta ahora hemos jugado sobre seguro, empleando para cada tipo de va riable su especificador correspondiente: %f para float, etc. Sin embargo, tam bién podemos usar printf( ) en un programa en que se pretenda averiguar el equivalente ASCII de un carácter determinado, por ejemplo. O lo que es lo mismo, realizar conversiones de tipo en la propia sentencia de impresión.
336 520 150 -336 65200
En primer lugar, como se puede esperar, la especificación %d imprime el número 336, como sucedía anteriormente. Sin embargo, obsérvense los re sultados obtenidos a continuación. El segundo número es 520, equivalente octal (es decir, base 8) del decimal 336 (5 x 64 + 2x8 + 0x1 = 336). De igual forma, 150 es el equivalente hexadecimal de 336. Por consiguiente, podemos emplear las especificaciones de conversión de printf( ) para convertir números en base 10 a números en base 8 ó 16. Sim plemente se trata de solicitar el número que se ha de imprimir con el especifi cador' correspondiente: %d, para obtener decimal, %o, para octal, y %x, para hexadecimal. No importa la forma en que el número aparezca original mente en el programa. Pero aún hay más. Si imprimimos —336 utilizando un °7od, no se produ ce ningún resultado extraño. Empero, este mismo número con especificación %u da como resultado 65200, no 336, como cabría esperar. El especificador %u corresponde a enteros sin signo (unsigned). El resultado procede de la forma de almacenamiento de números negativos en nuestro sistema de referen cia; concretamente se usa un método denominado “complemento a dos’’. En dicho método, los números 0 a 32767 se representan tal como están, mien tras que los números 32768 a 65535 se reservan para representar números ne gativos, siendo 65535 igual a -1, 65534 igual a -2, etc. Por tanto, -336 se representa como 65536 -336 o, lo que es lo mismo, 65200. Hay que tener 89
en cuenta que no todos los sistemas emplean este método para representar los enteros negativos; en cualquier caso, se debe sacar una moraleja: no espere que una conversión %u en su sistema se limite a un simple cambio de signo. Vayamos ahora con un interesente ejemplo que concierne a los caracte res. Ya lo hemos usado anteriormente, y se refiere a la utilización de printf( ) para encontrar el código ASCII de un carácter. Por ejemplo: printf("%c %d\n", ’ A',’ A') ;
produce A 65
como salida. A es la letra A, por supuesto, y 65 es el código ASCII decimal del carácter A. Se podría haber usado también %o para averiguar el código octal del mismo carácter. De este modo se dispone de una forma sencilla de conocer códigos ASCII de distintos caracteres, y viceversa. Si lo prefiere, también puede consultar el apéndice G, en que se encuentra una tabla completa. ¿Qué sucede si intentamos convertir en carácter un número mayor de 255? La respuesta la da la siguiente línea de programa y su resultado printf("%d %c\n", 336, 336); 336 P
El código ASCII decimal de P es 80, y ya se habrá percatado nuestro pers picaz lector que 336 es justamente 256 + 80. Aparentemente, el número se interpreta módulo 256 (en el argot matemático, módulo 256 significa el resto de la división del número por 256). Dicho de otra forma, cuando el ordena dor alcanza un múltiplo cualquiera de 256, comienza a contar de nuevo des de cero, por lo que 256 se tomará como 0, 257 como 1 , 5 1 1 como 255, 512 como 0, 513 como 1, etc. Como colofón final, intentaremos imprimir un entero (65616) mayor que el valor máximo (32767) permitido para int en nuestro sistema: printf("%1d %d \n”, 65616, 65616);
El resultado es 65616
80
Una vez más, el ordenador ha hecho su asunto con el módulo. En esta ocasión se comienza la cuenta en bloques de 65536. Los números comprendidos entre 32767 y 65536 habrían arrojado un resultado negativo debido a la forma peculiar de almacenamiento antes comentada. Si su sistema tiene un tamaño permitido para números enteros distinto del nuestro, el comportamiento que cabe esperar es el mismo, pero el rango será diferente al expuesto aquí.
No hemos agotado todas las posibilidades de combinaciones de datos y especificaciones de conversión, de manera que le aconsejamos que investigue por su cuenta. Mejor aún, intente comprobar si es capaz de predecir el resul tado de una determinada combinación antes de ejecutarla. Uso de scanf( )
Hasta ahora hemos hecho un uso bastante rudimentario de scanf( ); nos dedicaremos ahora a explorar las restantes posibilidades. Al igual que prinft( ), scanf( ) emplea una tira de caracteres de control y una lista de argumentos. La mayor diferencia entre ambas está en esta últi ma; printf( ) utiliza en sus listas nombres de variables, constantes y expre siones; scanf( ) usa punteros a variables. Afortunadamente, no se necesita saber ni lo más mínimo sobre punteros para emplear esta expresión; se trata simplemente de seguir las dos reglas que se dan a continuación: 1. Si desea leer un valor perteneciente a cualquiera de los tipos básicos, coloque el nombre de la variable precedido por un &. 2. Si lo que desea es leer una variable de tipo string (tira de caracteres), no use el &. El siguiente programa es válido: main()
{ int edad; float sueldo; char cachorro[30];
}
printf("Confiese su edad, sueldo y mascota favorita.\n"); scanf("%d %f", &edad, &sueldo); scanf ("%s", cachorro); /* en array de char no se usa & */ printf("%d %. 0f pts. %s\n ", edad, sueldo, cachorro);
y una posible salida sería: Confiese su edad, sueldo y mascota favorita. 82 9676123.50 rinoceronte 82 9676123 pts. rinoceronte
Scanf( ) considera que dos ítems de entrada son diferentes cuando están separados por blancos, tabulados o espacios. Va encajando cada especificador de conversión con su campo correspondiente, ignorando los blancos in termedios. Observe cómo se ha repartido la entrada en dos líneas. Podría mos también haber utilizado una o cinco, con la única condición de dejar al menos un carácter nueva línea, tabulado o espacio entre cada dos entra das. La única excepción es la especificación %c, que lee el siguiente carácter, sea blanco o no. 91
La función scanf( ) emplea un juego de especificadores de conversión muy semejante a! de printf( ). Las diferencias más sobresalientes son: 1. No existe la opción %g. 2. Las opciones %f y %e son equivalentes. Ambas aceptan un signo op cional, una tira de dígitos con o sin punto decimal y un campo para el exponente, también opcional. 3. Existe una opción %h para leer enteros short. Por cierto, scanf( ) no es la función más común para entrada de datos en lenguaje C. La venimos empleando desde el principio por su gran versatidad (puede leer datos de cualquiera de los tipos), pero hay que considerar que existen otras funciones de entrada en C, como getchar( ) y gets( ), que se acomodan mejor a tareas específicas, concretamente a la lectura de caracteres individuales y de tiras con espacios en blanco. Trataremos algunas de estas funciones más adelante, en los capítulos 6, 13 y 15.
La especificación de anchuras fijas de campos resulta muy útil cuando se desean imprimir columnas de datos. Al hacerse el ancho del campo por defecto equivalente a la anchura del propio número, el uso repetido, por ejemplo, de printf("%d %d %d\n", val1, val2, val3) ;
produciría columnas desalineadas en cuanto los números de una de ellas tuviesen un tamaño distinto. Así, la salida podría tener este tenebroso aspecto: 12 234 1222 4 5 23 22334 2322 10001
(Evidentemente, suponemos que el valor de las variables ha sido alterado entre ejecución y ejecución de la sentencia.) Por el contrario, se puede conseguir una salida nítida utilizando campos de anchura fija lo suficientemente grandes. Así, la sentencia val2, val3)
printf("Pepito Conejo corrio %. 2f leguas en 10 minutos.\n", distancia);
produciría Pepito Conejo corrio "4.23 leguas en 10 minutos.
en tanto que si la especificación de conversión se cambia a %l0.2f. el resul tado sería
Claves de utilización
printf ("%d9d %9d %d9d\n", val1,
Si se deja un blanco entre una especificación de conversión y la siguiente, queda garantizado que ningún número de se entremezclará con otro, incluso aunque supere el tamaño que tenga asignado. Este detalle se debe a que se imprimen todos los caracteres de la tira de control de printf( ), incluyendo los espacios. Cuando un número está destinado a aparecer dentro de una frase es a menudo conveniente especificar un campo igual o menor que el esperado. Con ello se consigue evitar que aparezcan blancos suplementarios que afea rían el texto. Compruébese con el ejemplo siguiente:
;
P e p i t o C o n e j o c o r r io
Hasta ahora hemos aprendido Qué es una tira de caracteres: unos caracteres puestos en fila Cómo escribir una tira de caracteres: “Esto es una serie de caracteres pues tos en fila” Cómo se almacena la tira: “Esto es una serie de caracteres puestos en fila \0” Dónde almacenar una tira: char frase[25] o static char frase[25] Cómo hallar la longitud de una tira: strlen(frase) Cómo imprimir una tira: printf(“Vos”, frase) Cómo leer tiras de una sola palabra: scanf (“%s”, nombre) Cómo definir constantes numéricas: #define DOS 2 Cómo definir constantes carácter: #define OLE '!' Cómo definir constantes tira: #define CUIDADO “¡No hagas eso!” Especificaciones de conversión E/S: %d %f %e %g %c %s %u %o %x Cómo hacer un ajuste fino de formatos de salida: %-10d %3.2f Cómo hacer conversiones: printf (“%d %o %c n”, OLE, OLE, OLE).
Cuestiones y respuestas
daría como resultado 12 4 22334
234 5 2322
14 .23 legu as en 10 m inu tos.
1222 23 10001
Cuestiones
1. Ejecute de nuevo el primer programa del capítulo; esta vez indique su nombre y apellido cuando le pregunte por su nombre. ¿Qué sucede?; ¿por qué? 93
2. Indicar la salida producida por cada uno de los fragmentos siguientes, suponiendo que forman parte de un programa completo: a. p r i n t f ( " V e n d i o l a p i n t u r a e n $‘%2. 2 f . \ n " , 2 . 3 4 5 e 2 ) ; b. printf ("%c%c%c\n", ' E' , 104, '\41'); C. #define Q "Interpreta Don Juan mejor que nadie." p r i n t f ( " %s \ n t i e n e % d c a r a c t e r e s . \ n " , Q , s t r l e n ( Q ) ) ; d. printf("Es lo Mismo % 2.2e que %2.2f?\n", 1201.0, 1201.0);
3. En la cuestión 2.c, ¿qué cambios habría que introducir para que la tira Q aparecíese entrecomillada en la salida? 4. ¡A la bonita búsqueda del error! define B farol define X 10 M a in ()
{ int edad; char nombre; prin tf("Introdu zca su nom b re."); scanf("%s", nombre); p r i n t f ( " M u y b i e n , %c, q u e e d a d t i e n e s ? \ n " , n o m b r e ) ; s c a n f ( "%f" , e d a d ) ; xp = edad + X;
}
p r i n t f ( " E s o e s u n %s P o r l o m e n o s t i e n e s %d . \ n " , B , x p ) ;
Respuestas 1. El programa revienta. La primera sentencia scanf( ) lee simplemente el primer nombre, dejando el segundo (o el apellido) sin tocar, pero almacenado en el buffer de entrada. (Este buffer es simplemente una zona de almacenamiento temporal que se usa para guardar entradas.) Cuando la segunda sentencia scanf( ) pregunta por su peso, recoge el dato anterior, su apellido, y lo toma como el peso. El programa queda así viciado e inservible. Por otra parte, si en lugar del nombre y apellido se contesta, por ejemplo, “Pepe 144”, la máquina tomará ese 144 como peso, aunque se haya escrito antes de solicitarlo. 2. a. V e n d i ó l a p i n t u r a e n $ 2 3 4 . 5 0 . b.
Eh! Nota: El primer carácter es una constante; el segundo, un entero decimal convertído en carácter, y el tercero, una representación ASCII de una constante carácter,
C. Interpreta Don Juan mejor que nadie. tiene 36 caracteres, d. Es lo m ism o 1.20E+03 que 1201.00?
3.
Recuerde las secuencias de escape del capítulo 3 y pruebe p r i n t f ( " \ " %s \ " n t i e n e %d c a r a c t e r e s . \ n " , Q , s t r l e n ( Q ) ) ;
4.
94
Línea 1: Se ha omitido #. farol debería ser “farol”. Línea 2: Se ha omitido #. Línea 6: n om b re debe ser un array; ch ar n om b re[25] serviría. Línea 8: Debe haber un \ n en la tira de control. Línea 10: El %c debería ser %s. Línea 11 : Al ser ed ad un entero, se deberá usar %d , no % f. Además, hay que poner no ed ad . Línea 12: xp no ha sido declarada. Línea 13: Es correcta, pero tendrá problemas por mala definición de B. Además, declaramos al programa culpable de falta de cuidado.
5 Operadores, expresiones y sentencias En este capítulo encontrará: • Introducción • Operadores fundamentales • Operador de asignación: = • Operador de adición: + • Operador de sustracción: — • Operador signo: — • Operador de multiplicación: * • Operador de división: / • Precedencia en operadores • Algunos operadores adicionales • Operador módulo: % • Operadores incremento y decremento: + + y - • Decremento:- • Precedencia • No se pase de listo • Expresiones y sentencias • Expresiones • Sentencias • Sentencias compuestas (bloques) • Conversiones de tipo • Operador de moldeado • Un programa ejemplo • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios
97
Operadores, expresiones y sentencias CONCEPTOS Operadores y operandos Hagamos aritmética Uso de while Expresiones Sentencias simples y compuestas Conversiones de tipo
PALABRAS CLAVE while OPERADORES + - * / % + + - - (tipo)
tulo. Entretanto, con la intención de que adopte un estado mental apropia do, presentamos nuestro pequeño programa de comienzo de capítulo, esta vez con un poco de aritmética.
/* zapatos1 */ #define TOPE 0.933 #define ESCALA 0.6167 main()
{
/* este programa convierte numero de zapatos en cm. de pie */ float zapato, pie; zapato = 42.0; pie = ESCALA * z a p a t o + T O P E ; printf("Numero z a p a t o centimetros printf ( " % 1 0 . 1 f % 1 6 . 2 f c m . \ n " , z a p a t o , p i e ) ;
pie\n");
} ¡Qué barbaridad! Nada menos que un programa con multiplicación y adi ción. Toma el número de sus zapatos (suponiendo que calce un 42) y le indi ca la longitud de su pie en centímetros. ¿Cómo dice? ¿Que usted lo hubiese calculado a mano mucho más rápido? Esa es una buena razón para llegar a la conclusión de que realizar un programa que calcule una sola talla de za patos es una solemne estupidez. Podríamos intentar mejorarlo reescribién dolo como programa interactivo, pero, a fuer de ser sinceros, estamos ro zando apenas el potencial real de nuestro ordenador. En realidad, lo que necesitamos es convencer al ordenador para que rea lice cálculos repetitivos. Después de todo, una de las misiones principales de un ordenador es la ejecución de cálculos aritméticos. En C se ofrecen varios métodos para efectuar operaciones repetidas; vamos a ver uno de ellos. Reci be el nombre de “bucle while”, y nos permite hacer un análisis más práctico de los operadores. He aquí una versión mejorada de nuestro programa de tallas de zapatos.
/* zapatos2 */ #define TOPE 0.933 #define ESCALA 0.6167 main () / * este programa convierte numero de zapatos en cm. de pie */ float zapato, pie; printf( "Numero zapato centímetros pie\n"); zapato = 30.0; while (zapato < 48. 5) { pie = ESCALA * zapato + TOPE; printf("%10.1f %16.2f cm.\n", zapato, pie); zapato = zapato + 1.0; } printf("Usted sabe donde le aprieta el zapato.\n");
Introducción En los capítulos 3 y 4 hemos comentado los tipos de datos que reconoce el C. Es el momento de estudiar la forma de manipular esos datos. El C ofre ce muchas posibilidades. Comenzaremos con las operaciones aritméticas bá sicas: suma, resta, multiplicación y división. Además, con el fin de hacer nues tros programas más útiles, daremos un primer repaso a los bucles en este capí
}
Una versión resumida de la salida en pantalla de zapatos2 sería:
(Por cierto, las constantes de conversión de tallas a centímetros se obtu vieron en una visita de incógnito a una zapatería. La única zapatería que ha bía por los alrededores era de caballeros. Ignoramos si estas constantes son idénticas para zapatos de señora; si está interesado en este particular, tendrá que averiguarlo por su cuenta.) Veamos cómo funciona el bucle while. Cuando el programa llegue por vez primera a la sentencia while, compruebe si la condición expresada entre paréntesis se cumple o no. En nuestro caso la condición es: zapato < 48.5
donde < es un símbolo que significa “menor que”. Pues bien, en el progra ma, el valor asignado a zapato es 30.0, que resulta evidentemente menor que 48.5; por consiguiente, la condición se cumple. En tal caso, el programa con tinúa con la siguiente sentencia, que convierte la talla en centímetros. A con tinuación imprime el resultado. La sentencia siguiente zapato = zapato + 1;
incrementa en 1.0 el valor de zapato, que ahora valdrá 31.0. En ese momen to el programa vuelve a la sentencia while a comprobar de nuevo la veraci dad de la información. ¿Y por qué en ese momento? Porque la línea siguien te es una llave de cierre (}), y estamos utilizando un juego de llaves ({ }) para marcar la extensión del bucle while; así, el ordenador sabe cuál es el grupo de sentencias que hay que repetir. Volvamos al programa. ¿31 es menor que 48.5? ¡Pues claro, qué pregunta! De nuevo nos metemos en el grupo de sen tencias, y se ejecutan las mismas operaciones (en la jerga informática, se lla ma “bucle” al conjunto de sentencias repetidas cíclicamente). El juego con tinúa hasta que zapato alcanza el valor 49.0. En ese instante, la condición
te al bucle. En nuestro ejemplo, dicha sentencia es la orden final de escritura printf( ). Se puede modificar con facilidad este programa con el fin de realizar to do tipo de conversiones. Por ejemplo, si hacemos ESCALA igual a 1.8 y TOPE igual a 32.0, tendremos un convertidor de grados centígrados a Fahrenheit. O bien, con ESCALA igual a 0.6214 y TOPE igual a 0, convertiremos kiló metros en millas. Si hace alguno de estos cambios, le convendrá cambiar también los mensajes de salida, en beneficio de la claridad. El bucle while es, por tanto, una herramienta flexible y conveniente para el control del programa. Nos ocuparemos ahora de los operadores funda mentales que se pueden utilizar en programación.
Operadores fundamentales En C se utilizan “operadores” para representar operaciones aritméticas. Por ejemplo, el operador + hace que se sumen los valores situados a su iz quierda y derecha. Si el nombre “operador” le resulta extraño... bueno, piense que habrá que llamar a esas cosas de alguna forma. Y puestos a elegir, esta rán de acuerdo con nosotros en que “operador” es una alternativa mejor que “esas cosas” o que “transactores aritméticos”, por poner un ejemplo. Estu diaremos ahora los operadores = , + , - , y /. (En C no existe el operador exponencial. En un capítulo posterior presentaremos una función que ejecu te esta tarea.) Operador de asignación: =
En C, el signo igual no significa “igual a”. En su lugar, es un operador de asignación de valores. La sentencia bmw = 2002;
asigna el valor 2002 a la variable bmw. Es decir, lo que hay a la izquierda del signo = es el nombre de la variable, mientras que la parte derecha es el valor de la misma. Llamamos al símbolo = “operador de asignación”. Re petimos, no interprete la sentencia como “bmw es igual a 2002”, sino como “asígnese el valor 2002 a la variable bmw”. En el caso concreto de este ope rador, la acción se ejecuta de derecha a izquierda. Quizá le parezca que la distinción que hacemos entre nombre y valor de una variable es un tanto histriónica. No hay tal; considere la siguiente sen tencia, muy común en programación: i = i + 1;
zapato < 48.5
se vuelve falsa, ya que 49 no es menor que 48.5. ¿Qué sucede ahora? Senci llamente, que el programa prosigue su tarea ejecutando la sentencia siguien
Desde un punto de vista matemático, carece de sentido. Si se suma 1 a un número finito, el resultado es, por supuesto, diferente del inicial. Consi derada como sentencia de asignación, sin embargo, resulta perfectamente co101
rrecta. Traducida a español, la sentencia significaría “encuentre el valor de la variable cuyo nombre es i. A tal valor, súmesele 1, y a continuación asíg nese este nuevo valor a la variable cuyo nombre es i”. Por el contrario, una sentencia como 2002 = bmw;
hace que se imprima el número 24, y no la expresión 4 + 20
Los operandos pueden ser aquí tanto constantes como variables. Así, la sentencia ganancia = salario + sobornos;
carece de sentido en C, porque 2002 es simplemente un número. No se puede asignar un valor a una constante por la sencilla razón de que ya lo tiene. Así pues, cuando se siente delante del teclado recuerde que la parte situada a la izquierda del signo = debe ser el nombre de la variable. Para aquellos que les guste llamar a las cosas por su nombre, indicare mos que lo que acabamos de llamar “parte situada” recibe en realidad el nom bre de “operando”. Operando es aquello sobre lo que opera el operador. Por ejemplo, comerse una hamburguesa se puede describir como una aplica ción del operador “comer” sobre el operando “hamburguesa” El operador básico de asignación en C es un poquito más llamativo que el de la mayoría de lenguajes. Pruebe a ejecutar el siguiente programa:
hace que el ordenador consulte los valores de las dos variables de la derecha, los sume y asigne el total a la variable ganancia. Se dice que el operador + es “binario” o “diádico”, en el sentido de que utiliza dos operandos. Operador de sustracción: —
El operador de sustracción hace que se reste el número situado a su derecha del situado a su izquierda. La sentencia llevoacasa = 224.00 - 24.00;
asigna el valor 20 a llevoacasa. /* resultados del torneo de golf */ main() { int jane, tarzan, chita;
Operador signo: —
chita = tarzan = jane = 68; printf(" chita tarzan jane\n"); printf("Primer recorrido %4d %8d %S8d\n", chita, tarzan, jane); }
La mayor parte de lenguajes de ordenador protestará en la sentencia de triple asignación que se realiza en este programa. En C se acepta sin proble mas. Las asignaciones se realizan de derecha a izquierda; así, jane tomará en primer lugar el valor 68; a continuación lo hará tarzán, y por último, chi ta. La salida de este programa será: chita Primer recorrido 68
tarzan 68
El signo menos se utiliza también para indicar o cambiar el signo alge braico de un valor. Por ejemplo, la secuencia pepe = -12; paco = -pepe;
asigna a paco el valor 12.
jane 68
Hay algunos otros operadores de asignación en C que funcionan de for ma distinta a la aquí comentada. Prometemos solemnemente hablar de ellos en un próximo capítulo. Operador de adición: +
El operador de adición hace que los dos valores situados a su izquierda y derecha se sumen. Por ejemplo, la sentencia printf("%d", 4 + 20);
Figura 5.1
Operadores unarios y binarios 103
p r i n t f ("cuadro granos sumados granos totales fraccion\n") ; printf(" cosecha\n"); total = actual = 1.0; /* comenzamos con un grano */ printf("%4d %15.2e %16.2e %12.2e\n", cont, actual, total, total/COSECHA); while (cont < CUADRADOS){
Cuando se utiliza el signo — con este sentido se dice que es un operador “unario”, indicando que emplea tan sólo un operando. Operador de multiplicación: *
La multiplicación se indica con el símbolo *. La sentencia
cont = cont + 1; actual = 2.0 * actual; /* duplica granos en cada cuadro */ total = total + actual;/* actualiza total */ printf("%4d %15.2e %15.2e %12.2e\n", cont, actual, total, total/COSECHA);
cm = 2.54 * pulg;
multiplica la variable pulg por 2.54 y asigna la respuesta a cm. ¿Necesita, por casualidad, una tabla de cuadrados? En C no existe un ope rador específico para cuadrados, pero podemos emplear la multiplicación. /* cuadrados */ main() /* produce una tabla de cuadrados */ { int num = 1; while (num < 21) { printf ("%10d %10d", num, num*num) ; n = n + 1;
}
}
Este programa imprime los 20 primeros enteros y sus cuadrados, por si quiere comprobarlo. Veamos ahora otro ejemplo más ilustrativo. Habrán oído hablar probablemente de la historia de aquel poderoso sul tán que deseaba recompensar a un estudiante que le había prestado un gran servicio. Cuando el sultán le preguntó la recompensa que deseaba, éste seña ló a un tablero de ajedrez y solicitó simplemente 1 grano de trigo por la pri mera casilla, 2 por la segunda, 4 por la tercera, 8 por la siguiente, y así suce sivamente. El sultán, que no debía andar muy fuerte en matemáticas, quedó sorprendido por la modestia de la petición, porque estaba dispuesto a otor garle riquezas mucho mayores; al menos, eso pensaba él. Según muestra el siguiente programa, nada más lejos de la realidad. En el programa se calcula el número de granos de trigo que corresponden a cada casilla y se acumula el total. Como el número de granos no es una cantidad que se maneje habi tualmente, se compara también con una estimación de la producción anual mundial expresada en granos.
}
} La salida comienza siendo bastante inocente: cuadro 1 2 3 4 5 6 7 8 9 10
granos sumados 1.00E+00 2.00E+00 4.00E+00 8.00E+00 1.60E+01 3.20E+01 6.40E+01 1.28E+02 2.56E+02 5.12E+02
granos totales 1.00E+00 3.00E+00 7.00E+00 1.50E+01 3.10E+01 6.30E+01 1.27E+02 2.55E+02 5.11E+02 1.02E+03
fraccion cosecha 2.50E-16 7.50E-16 1.75E-15 3.75E-15 7.75E-15 1.58E-14 3.18E-14 6.38E-14 1.28E-13 2.58E-13
Al cabo de 10 cuadrados el joven ha conseguido acumular algo más de 1.000 granos de trigo. Pero veamos lo que sucede en la casilla 52: 52
2.26E+15
4.52E+15
1.13E+00
¡La cantidad excede al total de la cosecha mundial! Si desea saber qué sucede en el cuadro 64, tendrá que ejecutar el programa usted mismo. Este ejemplo es ilustrativo del fenómeno de crecimiento exponencial. El crecimiento de la población mundial y la utilización de recursos energéticos por parte de la humanidad están siguiendo leyes semejantes. Operador de división: /
El símbolo / se utiliza en C para división. El valor a la izquierda de la / se divide por el que se encuentra a su derecha. Por ejemplo cuatro = 12.0/3.0;
/* trigo */ #define CUADRADOS 64 /* cuadrados del tablero */ #define COSECHA 7E14 /* cosecha mundial en granos */ main() { double actual, total; int cont = 0;
asigna a cuatro el valor 4.0. La división funciona de manera diferente en tipos flotantes y enteros. La división en punto flotante da como resultado un valor en punto flotante, en tanto que la división entre enteros produce un entero, es decir, un número sin parte decimal. Este hecho puede producir extraños resultados, como cuando se divide, por ejemplo, 5 entre 3, ya que el resultado no es entero. En C, cuando 105
se realiza una división entera, se descarta toda la parte decimal sin más trá mites. El proceso recibe el nombre de “truncado”. Ejecute el programa siguiente para aclarar ideas. En él se puede compro bar la diferencia entre división entera y de punto flotante. /* divisiones que hemos aprendido */ main()
{ printf printf printf printf printf
("division ("division ("division ("division ("division
entera: entera: entera: flotante: mixta: 7./4
5/4
es %d \n", 5/ 4) ; es %d \n", 6/3); 7/4 es %d \n", 7/ 4) ; 7. /4. es %2.2f \n", 7./ 4. ) ; es %2-2f \n", 7./ 4) ; 6/3
} Habrá observado que hemos incluido también un caso de división mixta, en el que se divide un número en punto flotante entre un entero. El C es un lenguaje bastante más permisivo que otros en cuestiones como mezclas de tipos, pero, como norma, procure no hacerlo. Veamos los resultados:
las operaciones en los órdenes expuestos, se obtienen como resultados fina les 255 y 192.5, respectivamente. Por su parte, el ordenador debe tener ideas propias, ya que, si sometemos la sentencia a su docto arbitraje, arroja como resultado, para manteca, el valor 205.0. Queda claro que el orden en que se ejecuten las operaciones afecta al re sultado. El C necesitará, por tanto, una forma no ambigua para escoger un orden determinado, es decir, unas reglas prefijadas que indiquen lo que ha de hacerse primero. Lo que se hace en C es escoger un orden salteado. A ca da operador se le asigna un nivel de precedencia: la multiplicación y división, por ejemplo, tienen mayor precedencia que la adición y sustracción, y por tanto se ejecutan antes. ¿Qué sucede si hay dos operadores con la misma pre cedencia? Bien, entonces se ejecutan según el orden en que aparecen en la sentencia. En la mayoría de los operadores, el orden elegido es de izquierda a derecha (una excepción que ya hemos apuntado es el operador asignación, = , que se ejecuta en sentido contrario). Volviendo a nuestra sentencia manteca = 25.0 + 60.0*n/ESCALA;
el orden de operación es: division division division division division
entera: 5 / 4 entera: 6/3 entera: 7 / 4 flotante: 7 . / mixta: 7 . / 4
4.
es 1 es 2 es 1 es es 1 . 7 5
60.0*n —el primer * o / de la sentencia. Suponiendo n = 6, tendremos 1.75
Observe que la división entera no redondea al entero más próximo, sino que siempre lo hace por defecto. Por otra parte, el caso de división mixta se ha resuelto como punto flotante; cuando en un programa C se encuentra un cálculo de esta clase, el entero se convierte en punto flotante antes de rea lizar la operación. Las propiedades de la división entera resultan ser bastante útiles en algu nos casos, como veremos en seguida en un ejemplo. Pero antes considere mos otro importante problema que aparece cuando en una misma sentencia se combinan varias operaciones. Este será el motivo de nuestra próxima sec ción.
60.0*n = 360.0. A continuación, 360.0/ESCALA —el segundo * o / de la sentencia. Si ESCALA es igual a 2.0, el resultado será 180.0. Seguidamente, 25.0 + 180.0 —el primer + o — de la sentencia. Obtenemos así el resulta do final, 205.0. Hay bastante gente a la que le gusta representar el orden establecido para la evaluación en forma de diagrama. Estos diagramas reciben el nombre de “árbol de expresiones”. Veamos un ejemplo.
Precedencia en operadores
Supongamos la línea manteca = 25.0 + 60.0*n/ESCALA;
Esta sentencia contiene una suma, una multiplicación y una división. La cuestión es: ¿Cuál se ejecuta en primer lugar? ¿Se suman 25.0 y 60.0, el re sultado 85.0 se multiplica por n y lo que se obtenga se divide por ESCALA? ¿O bien se multiplica primero 60.0 por n; el resultado se suma a 25.0, y la respuesta se divide por ESCALA? Vayamos por partes. Asignemos a n el va lor 6.0, y a ESCALA el valor 2.0. Sustituyendo estos valores, y ejecutando
Figura 5.2
Arboles de expresiones con operadores, operandos y orden de evaluación
El diagrama demuestra cómo se reduce la expresión original por etapas hasta llegar a un valor. ¿Qué sucede si uno desea realizar la suma antes que la división? Bien, entonces se puede escribir:
Puesto que los paréntesis tienen la prioridad más alta, si vamos de izquierda a derecha en la expresión, el primer par de paréntesis que nos tropezamos es (2 + 5), así es que calculamos su contenido y obtenemos: max = tanteo = -7*6 + (4 + 3*(2 + 3))
harina = (25.0 + 60.0*n)/ESCALA;
Cualquier cosa que se encierre entre paréntesis se ejecuta con preferencia sobre las demás. Dentro de los paréntesis se mantienen las reglas ya comen tadas. En este ejemplo se efectuará en primer lugar la multiplicación, y des pués la suma. Con esto queda completada la evaluación del paréntesis. Uni camente entonces se realiza la división por ESCALA. Podemos ahora hacer una tabla en que se resuman todas las reglas con operadores que hemos utilizado hasta el momento. En el apéndice C se pre senta una tabla que comprende todos los operadores. Tabla 5-1. Operadores en orden decreciente de precedencia OPERADORES
El siguiente par de paréntesis es (4 + 3*(2 + 3)), de modo que nos toca de nuevo evaluar su contenido, es decir, la expresión 4 + 3*(2 + 3). ¡Ajá, más paréntesis! Aquí lo que hay que hacer en primer lugar es calcular 2 + 3. La expresión que tenemos ahora es max= tanteo = -7*6 + (4 + 3*5)
Todavía tenemos que acabar con los paréntesis de fuera. Como * tiene preferencia sobre +, la expresión siguiente que se obtiene será max = tanteo = -7*6 + (4 + 15)
ASOCIATIVIDAD izquierda a derecha
() —(unario)
izquierda a derecha
*/
izquierda a derecha
+ —(sustracción)
izquierda a derecha
=
derecha a izquierda
Obsérvese que los dos empleos del signo menos tienen diferente priori dad. La segunda columna indica la forma en que el operador se asocia a sus operandos. Por ejemplo, el signo menos unario queda asociado a la cantidad escrita a su derecha, y el operador división divide la cantidad de su izquierda entre la situada a su derecha. Comprobemos estas reglas de precedencias y prioridades con un ejemplo más complicado. /* test de precedencia */ main {
y a continuación max = tanteo = -7*6 + 19
¿Qué viene ahora? Si piensa que es 7*6, está equivocado. Observe que el — unario tiene prioridad mayor que el *. Se trata de un cambio de signo, por lo que 7 se transforma en —7, y seguidamente se multiplica —7 por 6. Nuestra expresión original ha quedado reducida a max = tanteo = -42 + 19
y la ejecución de la suma la transforma en max = tanteo = -23
En ese momento, a tanteo se le asigna el valor —23, y, por último, max toma también el valor —23. Recuérdese que el operador = asocia de dere cha a izquierda.
int max, tanteo;
}
max = tanteo = -(2 + 5)*6 + (4 + 3*(2 + 3)); printf ( "max = %d \n", max);
¿Qué valor imprimirá este programa a la salida? Intente calcularlo “de cabeza”, y a continuación ejecute el programa o lea la siguiente descripción para comprobar su resultado (estamos seguros de que es correcto).
Algunos operadores adicionales El C tiene alrededor de 40 operadores, algunos de los cuales se utilizan mucho más que otros. Los que hemos repasado hasta ahora son los más co munes; a esa lista vamos a agregar ahora tres operadores adicionales que re sultan bastante útiles. 109
Operador módulo: %
Operadores incremento y decremento: + + y--
El operador módulo (o resto) se emplea en aritmética de números ente ros. Proporciona el resto de la división entera (es decir, sin decimales) del número entero situado a su izquierda entre el situado a su derecha. Por ejem plo, 13 % 5 (léase 13 módulo 5) es 3, ya que 13 entre 5 da un cociente de 2 y un resto de 3. No incordie con este operador en números de punto flotante; simplemen te, no funciona. A primera vista, este operador le suena a uno como una herramienta eso térica puesta ahí para deleite de los matemáticos, pero en realidad se trata de algo práctico y con grandes posibilidades. Supongamos, por ejemplo, que usted desea realizar un programa de facturación mensual, en el que hay que añadir una cierta cantidad extra al final de cada trimestre. Veamos cómo se puede utilizar este operador para controlar el flujo del programa. Simple mente, haga que el ordenador calcule el número de mes módulo 3 (mes % 3) y compruebe si el resultado obtenido es 0. Si es así, añada la cantidad suple mentaria que corresponda. Comprenderá mejor el sistema cuando estudie mos las sentencias condicionales (sentencias if) más adelante. Veamos ahora un ejemplo en que se usa %
El operador incremento realiza una tarea muy simple: incrementa (aumen ta) el valor de su operando en 1. Se ofrecen en C dos variedades. En la pri mera de ellas, el + + aparece antes de la variable afectada, es el llamado modo “prefijo”. En la segunda, el + + se encuentra situado detrás de la variable. A esta variedad la denominaremos modo “sufijo”. La diferencia entre ambos modos reside en el preciso momento en que se realiza la operación de incremento. En primer lugar prestaremos atención a las semejanzas, y volveremos más adelante a las diferencias. El ejemplo si guiente demuestra el funcionamiento de ambos operadores. /* sumauno */ Main() /* incremento: prefijo y sufijo */
{
int ultra = 0, super = 0; while (super < 6) {
super++; ++ultra; printf ("super = %d, ultra = %d\n", super, ultra);
} /*segamin*/ / * c o n v ie r t e s e g u n d o s e n m i n u t o s y s e g u n d o s * / #d efine SM 60
El programa da como resultado
/ * s e g u n d o s e n u n m in u t o * /
m ai n( )
{ i n t s e g , m in , r e s t o ; p r in t f ( " C o n v ie r t e segundos en m in utos y segundos\n"); p r in t f ( " I n t r o d u z c a segundos a c o n v e r t ir . scanf("% d", & seg); /* se le e el numero de segundos */ m in = seg /SM ; /* num ero tru ncad o de m inutos */ resto = seg % S M ; /* num ero de segundos de resto*/ p r in t f ( " % d s e g u n d o s s o n % d m i n u t o s , % d s e g u n d o s . \ n " , seg , m in, resto );
\n");
}
super super super super super =
= 1, ultra = 2, ultra = 3, ultra = 4, ultra 5, ultra = 5
= = = =
1 2 3 4
¡Qué exageración! ¡Hemos contado hasta cinco dos veces! ¡Simultánea mente! (Si desea contar aún más, simplemente cambie el límite establecido en la sentencia while.) Para ser honestos, confesemos que hubiésemos obtenido exactamente el mismo resultado con las sentencias:
Un ejemplo de la salida de este programa podría ser el siguiente: C onvie rte se gun dos e n m inu tos.y se gund os I n t r o d u z c a s e g u n d o s a c o n v e r t ir . 234
super = super + 1; ultra = ultra + 1;
234 seg undo s son 3 m inu tos, 54 seg undo s.
Un pequeño (o gran) defecto de este programa interactivo es que se eje cuta procesando un único valor de entrada. ¿Podría usted indicar una mane ra de que el programa solicite repetidamente nuevos números para calcular? Trataremos este problema en la siguiente sección de este capítulo, pero nos agradaría mucho saber que ha encontrado su propia solución por su cuenta.
Por cierto, son sentencias bastante compactas. Entonces, ¿para qué mo lestarse creando no una, sino dos formas abreviadas? En primer lugar, la forma compacta hace los programas más elegantes y fáciles de seguir. Estos operadores dan al programa un cierto glamour que no deja de ser agradable a la vista. 111
cia while, nuevo incremento en uno de talla y nueva comparación. El ciclo se repite hasta que talla excede el valor prefijado. Observe que hemos inicializado talla a 29.0 en lugar de 30.0 para compensar el incremento previo a la primera comparación. BUCLE while
PRIMERO, INCREMENTA TALLA A 30
SEGUNDO, EVALUA EL TEST
TERCERO, EJECUTA ESTAS SENTENCIAS
CUARTO, VUELVE AL COMIENZO DE BUCLE
Figura 5.3
Una pasada por el bucle
Podemos, por ejemplo, reescribir parte del programa zapatos2 de la si guiente forma: talla = 30.0; while (talla < 48.5) {
pie = ESCALA*talla + TOPE; printf ("%10.1f %16.2f cm. \n", talla, pie); ++talla;
} Pero todavía no le hemos sacado todo el partido a estos operadores. Se puede abreviar más aún el fragmento anterior de esta forma: talla = 29.0; while (++talla < 48.5) { pie = ESCALA*talla + TOPE; printf("%10.1f %16.2f cm.\n”, talla, pie);
} Aquí se encuentran combinados el proceso de incremento de nuestro ín dice y la parte comparativa del bucle while; los dos integran la misma expre sión. Este tipo de construcción es tan común en C que merece un repaso más detallado. En primer lugar, ¿cómo funciona? De forma muy sencilla. Se aumenta en uno el valor de talla y se compara con 48.5. Si es menor, el pro grama se introduce en el bucle, ejecutándose éste una vez. Vuelta a la senten
En segundo lugar, ¿qué es lo que resulta tan excelente en esta representa ción? Es más compacta y, lo que es más importante, consigue ubicar en un lugar los dos procesos que controlan el bucle. El primero es el test de compa ración: ¿seguimos con el bucle o no? En este caso, el test consiste en compa rar la talla del zapato con 48.5. El segundo proceso cambia un elemento del test; en nuestro ejemplo, la propia talla del zapato. Supongamos que olvida ra incrementar la talla a cada ciclo del bucle. En ese caso, talla sería siempre menor que 48.5, y el bucle no acabaría nunca. El ordenador se limitaría a repetir una y otra vez las mismas sentencias impasiblemente, y nos hallaría mos atrapados en lo que en el argot se llama un “bucle infinito”. La salida, por demás, sería bastante monótona, por lo que imaginamos que acabaría por perder el interés por la misma e intentaría detener el ordenador de algún modo (es conveniente tener localizada la tecla de parada de ejecución, por si se da el caso). Si tenemos en un mismo sitio el test y el incremento de índice del bucle resulta más sencillo recordar la necesidad de incluir un cambio den tro del bucle. Otra ventaja del operador incremento es que genera un código compila do ligeramente más eficiente, ya que su estructura se asemeja más al código máquina real. Por último, estos operadores tienen una característica adicional que pue de ser muy útil en ciertas situaciones delicadas. Para comprenderla mejor, observemos el siguiente programa. main() { int a = 1, b = 1; int amas, masb; 113
amas = a++; /* sufijo */ masb = ++b; /* prefijo */ printf(" a amas b masb\n"); printf ("%3d %5d %5d %5d\n", a, amas, b, masb) ;
} Si lo escribe correctamente, y nosotros recordamos correctamente, debe obtener un resultado como éste a amas b masb 2
1
2
pero, con sinceridad, nadie le considerará un programador “serio” de C si se anda con esas expresiones. Le sugerimos que preste atención a los distintos ejemplos de operadores incremento que irán apareciendo a lo largo del libro. Cuestiónese si hubiese usado uno u otro, o si las circunstancias aconsejaban la selección de uno de terminado. Hablando de ejemplos, aquí viene otro más: ¿Duermen alguna vez los ordenadores? Por supuesto que lo hacen, pero generalmente no nos informan de ello. Este programa revela lo que sucede en realidad.
2
Tanto a como b se han incrementado en 1, como era de esperar. Sin em bargo, amas contiene el valor de a antes de que éste fuera cambiado, en tan to que masb toma el valor de b tras el incremento. He ahí la diferencia pro metida entre los modos prefijo y sufijo del operador incremento.
/* ovejas */ #define MAX 40 main()
{ int cont = 0; printf("Contare ovejitas para dormirme.\n"); while (++cont < MAX)
amas = a + + sufijo: a cambia después de ser usado su valor masb = + + b prefijo: b cambia antes de ser usado su valor
printf ("%d millones de ovejas y aun no me he dormido... \n", cont);
}
PRIMERO, incrementa a en uno DESPUES, multiplica a por dos y
asigna el resultado aq
PRIMERO, multiplica a por dos y
asigna el resultado a q
printf("%d millones de ovejas y zzzzzzzz.......\n", cont);
Ejecútelo y compruebe si hace lo que usted cree. Puede, por supuesto, que el valor de MAX sea diferente en su ordenador. (Por cierto, ¿qué hubie ra sucedido si hubiésemos empleado la forma sufija del operador de incre mento en lugar de la prefija?)
DESPUES, incrementa a en uno
Figura 5.4
Prefijo y sufijo
Cuando se utiliza el operador en solitario en una sentencia, como ego + +;, no importa la modalidad escogida. Sí importa, y mucho, cuando el operador y su operando forman parte de una expresión mayor, tal como la sentencia de asignación que acabamos de ver. En una situación como ésa, uno debe tener bastante claro el resultado que desea obtener. Sí recordamos la vez an terior, en que empleábamos + + en un while
Decremento: --
Existen también en C dos operadores decremento que se corresponden con los incremento que acabamos de comentar. En ellos se utiliza -- en lugar de + + . --cont; /* forma prefijo del operador decremento */ cont--; /* forma sufijo del operador decremento */
En el ejemplo siguiente, además de utilizar el operador decremento, de mostramos claramente que el ordenador es también capaz de hacer pinitos en poesía:
while (++talla < 18.5)
obtuvimos una tabla hasta el número 48. Si hubiésemos usado la forma sufi jo, talla + + , la tabla habría llegado hasta el 49, ya que talla se incrementa ría después de realizada la comparación, en lugar de antes. Evidentemente, podríamos haber conseguido el mismo resultado con talla = talla + 1;
/* botellas */ #define MAX 100 main()
{ int cont = MAX + 1; while (--cont > 0)
{
115
printf("%d botellas de vino en el estante, %d botellas!\n", cont, cont); printf("Alguien paso por delante y que fue de ellas?\n"), printf("Solo quedan %d botellas !\n", cont - 1); } }
La variable n se incrementa únicamente después de haberse realizado la operación completa. Lo que indica la precedencia es que el ordenador + + afecta únicamente a n; también nos indica cuándo se empleará n para la eva luación de la expresión, pero el momento en que n se incrementa viene deter minado por la propia naturaleza del operador incremento.
La salida comienza así:
No se pase de listo
100 botellas de vino en el estante, 100 botellas! Alguien paso por delante y que fue de ellas? Solo quedan 99 botellas! 99 botellas de vino en el estante, 99 botellas! Alguien paso por delante y que fue de ellas? Solo quedan 98 botellas!
Los operadores incremento pueden acabar por jugarle una mala pasada si pretende hacer todo de una vez antes de dominarlos por completo; con ellos puede llegar a cometer errores estupendos. Por ejemplo, podría parecerle que se puede mejorar el programa que hemos puesto como ejemplo unas páginas antes, cuya finalidad era imprimir enteros y sus cuadrados. Aprovechando nuestra nueva adquisición se podría reemplazar el bucle while por
Sigue insistiendo un rato, y termina:
while
(num < 21)
{ printf ("%10d, %10d\n", num, num*num++) ;
1 botellas de vino en el estante, 1 botellas! Alguien paso por delante y que fue de ellas? Solo quedan 0 botellas!
Aparentemente, nuestro aprendiz de poeta tiene algún problema con los plurales, pero ya lo enmendaremos cuando veamos operadores condiciona les en el capítulo 7. Por cierto, el operador > significa “mayor que”. Al igual que < , es un “operador de relación”. Lo veremos en profundidad también en el capítulo 7.
} Parece bastante razonable. Imprimimos el número num, lo multiplicamos por sí mismo, para obtener un cuadrado, y a continuación aumentamos num en 1. De hecho, este programa puede incluso funcionar en algunos sistemas, pero no en todos. El problema reside en que cuando printf( ) toma los valo res a imprimir puede perfectamente empezar por evaluar el último argumen to en primer lugar, e incrementar num antes de capturar el argumento ante rior. En ese caso, en lugar de obtener como resultado 5
25
Precedencia
Los operadores de incremento y decremento tienen una precedencia de asociación muy alta; tan sólo son superados por los paréntesis. Por ello, x*y + + significa (x)*(y + +) y no (x*y) + + ; por otra parte, no puede ser de otra forma, ya que esta última expresión carece de sentido (los operadores de incremento y decremento afectan a una variable, y el producto x*y no lo es, auque lo sean sus partes). No confunda la precedencia de los operadores con el orden de evalua ción. Suponga que tenemos y = 2; n = 3; proximo = (y + n++)*6;
¿Qué valor tomará próximo? Si sustituimos variables, proximo = (2 + 3)*6 = 5*6 = 30
podemos encontrarnos con 6
25
El C otorga al compilador libertad absoluta para organizar los argumen tos de una función como mejor le parezca, incluyendo la decisión del orden en que se toman, y la de si la evaluación de expresiones se realiza al tiempo o después. Esta política aumenta la eficiencia de los compiladores, pero le puede crear problemas si utiliza operadores incremento dentro de una función. Otra posible fuente de problemas es una sentencia como la siguiente: resp =
num/2
+ 5*(1 + num++);
De nuevo el error puede surgir porque el compilador no ejecute las cosas en el orden que nosotros pensábamos. Lo lógico sería pensar que empezaría por num/2, y seguiría por la línea. Pues bien, en realidad puede que calcule el último término antes, realice el incremento y use el nuevo valor de num 117
Expresión
para evaluar num/2. Muy sencillo, no hay garantía de que se vaya a compor tar de una u otra manera. Por otra parte, es bastante sencillo evitar este tipo de problemas: 1. No utilice operadores de incremento o decremento en variables que se emplean más de una vez como argumento de una función. 2. No utilice operadores de incremento o decremento en variables que se empleen más de una vez en una misma expresión.
Expresiones y sentencias
Valor +
-4+6 c = 3 + 8 5 > 3 6 + (c = 3 + 8)
1 17
¡Qué extraña parece la última! Sin embargo, es perfectamente legal en C, ya que se trata de la suma de dos subexpresiones, cada una de las cuales tiene un valor. Sentencias
Hasta ahora hemos estado utilizando los términos “expresión” y “sen tencia” en los capítulos anteriores, sin habernos detenido a analizarlos en profundidad. Es el momento de hacerlo. Las sentencias constituyen las eta pas básicas en que se desarrolla un programa en C, y, a su vez, la inmensa mayoría de sentencias C están formadas por expresiones. Parece lógico, en consecuencia, empezar por estudiar las expresiones, y así lo haremos aquí. Expresiones
Se llama expresión a una combinación de operadores y operandos (recor damos que el operando es aquello sobre lo que actúa el operador). La expre sión más simple posible es un operando aislado; a partir de él se pueden ir construyendo expresiones de mayor o menor complejidad. Ejemplos de ex presiones son: 4 -6 4+21 a*(b + c/d)/20 q = 5*2 x = ++q %3 q > 3
Como ve, los operandos pueden ser variables, constantes o combinacio nes de ambos. Algunas expresiones son combinaciones de expresiones meno res, que llamamos subexpresiones. Por ejemplo, en el cuarto renglón, c/d es una subexpresión de la expresión total. Una propiedad de las expresiones C que conviene destacar es que cada expresión tiene un valor. Para averiguarlo, basta con realizar las operaciones en el orden dictado por la precedencia de los operadores. En los ejemplos anteriores, algunas expresiones tienen un valor evidente; otras, sin embargo, son más complicadas. ¿Qué hay de las que contienen signos = ? Simplemen te tienen el mismo valor que adquiere la variable situada a su izquierda. ¿Y la expresión q > 0? Las expresiones de relación toman valor 1 cuando son ciertas, y valor 0 si son falsas. A continuación presentamos algunas expresio nes y sus valores correspondientes:
Las sentencias son las piezas con que se construye un programa. Un pro grama, en realidad, es simplemente un conjunto de sentencias con algo de puntuación ortográfica por medio. Una sentencia, además, es una instruc ción completa para el ordenador. En C se significan las sentencias acabándo las en punto y coma. Por tanto patas = 4
es una expresión (que, a su vez, puede formar parte de una expresión ma yor), mientras que patas = 4 ;
es una sentencia. ¿Qué es lo que caracteriza a una sentencia? Debe completar una acción. La expresión 2+2
no es una instrucción completa. Indica al ordenador que sume dos y dos, pe ro no le dice qué tiene que hacer con el resultado. Sin embargo hijos =2 + 2 ;
está ordenando a la máquina que almacene el resultado (4) en la dirección de memoria etiquetada hijos. Una vez ejecutada esta tarea, el ordenador se dirigirá a realizar la siguiente que corresponda. Hasta ahora nos hemos tropezado con cuatro clases de sentencias. A con tinuación presentamos un ejemplo en que se utilizan las cuatro. /* sumame */ main()
{
/* calcula la suma de los 20 primeros enteros */
int cont, suma;
/*
sentencia de declaracion */ 119
cont = 0;/*sentencia de asignación */ suma = 0; /*ídem*/ while (cont++<20) /*sentencia*/ suma = suma + cont ; /*while*/ printf ("suma = %d\n", sum) ; /*sentencia de función*/
}
La sentencia while pertenece a una clase que a menudo se ha dado en lla mar “sentencias estructuradas”, por poseer una estructura más compleja que la de una simple sentencia de asignación. Encontraremos una gran variedad de sentencias estructuradas en capítulos posteriores. Sentencias compuestas (bloques)
Veamos los distintos tipos. En este momento debe estar ya bastante fami liarizado con la sentencia de declaración. Por si acaso, le recordaremos que sirve para establecer los nombres y el tipo de variables, y hace que el ordena dor reserve posiciones de memoria para cada una de ellas. La sentencia de asignación es el caballo de batalla de la mayoría de los programas; sirve para asignar valores a las variables. Está formada por un nombre de variable, seguido del operador asignación ( = ), seguido de una expresión, seguida de un punto y coma (ya puede tomar aire). Observe que la sentencia while incluye una sentencia de asignación como parte de la misma. Las sentencias de función se encargan de que las funciones hagan lo que deben hacer. En el ejemplo anterior se llama a la función printf( ) para que imprima algunos resultados de salida. La sentencia while se compone de tres partes distintas. En primer lugar, está la palabra clave while; a continuación, entre paréntesis, aparece la con dición a comprobar; finalmente, viene la sentencia a ejecutar, si la condición se cumple. Dentro del bucle se acepta una sola sentencia, si bien ésta puede ser una sentencia simple, como en el ejemplo, o compuesta, en cuyo caso se utilizarán llaves para marcar los límites de la misma. Si se trata de una sen tencia simple, las llaves no son necesarias. Trataremos las sentencias com puestas dentro de un momento.
Se denomina “sentencia compuesta” a un conjunto de dos o más senten cias agrupadas y encerradas entre llaves; recibe también el nombre de “blo que”. Ya hemos empleado uno en nuestro programa zapatos2 con el fin de permitir que el bucle while pudiese realizar varias sentencias. Compárense los dos siguientes fragmentos de programa: /* fragmento 1 */ indice = O; while (indice++ < 10) sam = 10*indice + 2; printf("sam = %d\n", sam); /* fragmento 2 */ indice = 0; while (indice++ < 10) { sam = 10*indice + 2; printf("sam = %d\n", sam);
}
Retorno del bucle Retorno del bucle
Falso, ir a sentencia siguiente Observe notación prefijo; pez se incrementa antes de la comparación
Figura 5.5
Estructura de un bucle while sencillo
Figura 5.6
Buble while con sentencia compuesta 121
En el fragmento 1, la única sentencia incluida en el bucle es la sentencia de asignación, ya que, en ausencia de llaves, el while se extiende hasta el final del siguiente punto y coma. La salida se imprimirá una sola vez, al finalizar el bucle. En el fragmento 2, las llaves hacen que ambas sentencias constituyan una sentencia compuesta, obteniéndose una salida impresa a cada vuelta del bu cle. A efectos de éste, las dos sentencias se consideran una sola dentro de la estructura de while.
DETALLES DE ESTILO Si observamos atentamente los dos fragmentos anteriores, nos percata remos del modo en que se aprovecha la identación para reflejar el límite del bucle. Al compilador no le afecta para nada esta indentación; por su par te, los límites del bucle se extienden hasta el siguiente punto y coma o hasta la llave de cierre, según sea el caso. Para el que esté revisando el programa, sin embargo, este sistema resulta muy útil, ya que permite decidir al mo mento hasta dónde se extiende este bucle. Acabamos de mostrar un estilo popular de colocación de llaves en un bloque. Otra forma bastante común de hacerlo es la siguiente: while (indice++ < 10)
{ sam = 10*indice + 2; printf ("sam = %d\n", sam); }
En esta última manera se destaca sobre todo el bloque de sentencias que constituyen la sentencia compuesta. En la otra se hace mayor énfasis en la pertenencia de ese bloque de sentencias a un bucle while. Por lo que con cierne al compilador, una vez más, ambas formas son idénticas. En resumen, es aconsejable utilizar la indentación como herramienta que ayude a destacar la estructura del programa.
RESUMEN: EXPRESIONES Y SENTENCIAS Expresiones: Una expresión es una combinación de operadores y operandos. La expresión más simple es una constante o variable sin más, como 22 o pepito. Ejemplos de algunas expresiones más complicadas podrían ser: 55 + 22, y
vap = 2*(vip + (vup = 4)).
Sentencias: Una sentencia es una orden dada al ordenador. Existen sentencias simples y compuestas. Las sentencias simples terminan con un punto y coma. Por ejemplo: 1. sentencias de declaración: 2. sentencias de asignación: 3. sentencias de llamada a funciones: 4. sentencias de control:
int dedos; dedos = 12; printf(“%d\n”, dedos); while (dedos < 20) dedos = dedos + 2;
5. sentencia nula:
;
Las sentencias compuestas, o bloques, están formadas por una o más senten cias (que, a su vez, pueden ser compuestas) encerradas entre llaves. En el si guiente bucle while se muestra un ejemplo: while (edad < 100) sapiencia = sapiencia + 1;
Conversiones de tipo En general, en una sentencia o expresión se emplean variables y constan tes de un solo tipo. Sin embargo, si en un momento dado se mezclan dichos tipos, el C no se molesta en seguir la pista del eventual error, como hace, por ejemplo, el PASCAL. En su lugar, utiliza una serie de reglas para efec tuar automáticamente conversiones de tipo. Esta característica del C puede ser muy útil en ocasiones, pero también un arma de doble filo, en especial cuando se mezclan tipos inadvertidamente (existe en muchos sistemas UNIX un programa llamado lint, que comprueba este tipo de “colisiones”). Es con veniente, por tanto, tener una idea razonablemente clara del funcionamiento de las conversiones de tipo. Las reglas básicas son las siguientes: 1. En cualquier operación en que aparezcan dos tipos diferentes se eleva la “categoría” del que la tiene menor para igualarla a la del mayor. Este proceso se conoce con el nombre de “promoción”. 2. El rango o categoría de los tipos, de mayor a menor, es el siguiente: double, float, long, int, short, char. Los tipos unsigned tienen el mis mo rango que el tipo a que están referidos. 3. En una sentencia de asignación, el resultado final de los cálculos se reconvierte al tipo de la variable a que están siendo asignados. El pro ceso puede, pues, ser una “promoción” o una “pérdida de rango”, según que la variable a asignar sea de categoría superior o inferior. 123
La promoción suele ser un proceso bastante tranquilo, que pasa inadver tido fácilmente. La pérdida de rango, por el contrario, puede originar autén ticas catástrofes. La razón es muy sencilla: el tipo de menor rango puede no ser lo bastante amplio como para albergar el número completo. Una variable char, por ejemplo, puede contener el entero 101, pero no el 22334. En el programa siguiente se puede comprobar el funcionamiento de estas reglas. / * conversiones */ main () { char ch; int i ; float fl;
fl = i = ch = ’A’; /* linea 8 */ printf ("ch = %c, i = %d, fl = %2.2f\n", ch, i, fl); ch = ch + 1; /* linea 10 */ i = fl + 2*ch; / * linea 11 */ fl = 2.0*ch + i; /* linea 12 * / printf ("ch = %c, i = %d, fl = %2.2f\n", ch, i, fl); ch =2.0e30 /* linea 14 * / print f ( "Ahora ch = %c\n", ch) ;
} La ejecución de conversiones produce el siguiente resultado: ch = A, i = 65, fl = 65.00 ch = b, i = 197, fl = 329.00 Ahora ch =
Veamos qué ha pasado. Líneas 8 y 9: el carácter ‘A’ se almacena como carácter en la variable ch. La variable de tipo int i recibe la conversión a entero del carácter, que es 65. Por último, fl almacena la conversión en punto flotante de 65, que corres ponde a 65.00. Líneas 10 y 13: la variable carácter ‘A’ se convierte en el entero 65, al cual se le suma 1. Dicho valor se debe asignar de nuevo a la variable ch, por lo que se reconvierte en carácter, que ahora será el B, y se almacena en ch. Líneas 11 y 13: el valor de ch se convierte en entero (66) para poder multi plicarlo por 2. El resultado, entero, se transforma en flotante para sumarlo (132) a fl. El resultado de la suma (197.00) se convierte en entero (tipo int) y se almacena en i. Línea 12 y 13: el valor de ch (‘B’) se convierte a punto flotante para po der sumarse a 2.0. El valor de i (197) se convierte también en punto flotante por efecto de la adición, y el resultado (329.00) se almacena en fl. Líneas 14 y 15: aquí intentamos un caso de pérdida de rango, haciendo ch igual a un número muy grande. El resultado obtenido es bastante pobre.
Por inexplorados procesos de incubación y truncado nuestro sistema ha lle gado a asignar a ch un valor de carácter no imprimible. En realidad, existe otro proceso de conversión que no hemos menciona do aquí. Con el fin de conservar al máximo la precisión numérica, todas las variables y constantes float se convierten en double cuando se realizan cálcu los aritméticos con ellas; así se reduce enormemente el error de redondeo. Por supuesto, la respuesta final se reconvierte a float, si ese es el tipo declarado. No tenemos que preocuparnos para nada de este último tipo de conversión, pero es agradable saber que el compilador vela por nuestros intereses. Operador de moldeado
Usualmente, lo mejor es mantenerse apartado de las conversiones de ti pos, en especial de las pérdidas de rango. No obstante, existen ocasiones en que es conveniente, y hasta necesario, hacer alguna conversión. Las conver siones discutidas hasta ahora se ejecutan automáticamente, pero es posible también especificar un tipo concreto de conversión que se desee. El proceso se denomina “moldeado” o “ahormado”, y se realiza anteponiendo a la can tidad a “moldear” el nombre del tipo requerido entre paréntesis. El conjun to de paréntesis y tipo recibe el nombre de “operador de moldeado”. La for ma general de dicho operador es (tipo)
donde se ha de sustituir el verdadero nombre del tipo a imponer en lugar de la palabra “tipo”. En el siguiente ejemplo se supone que ratones es una variable de tipo int. La segunda línea contiene dos “moldeadores” a tipo int. ratones = 1.6 + 1.7; ratones = (int) 1.6 + (int) 1.7;
El primer renglón del ejemplo emplea conversión automática. Primero se suman 1.6 y 1.7, dando como resultado 3.3. Este valor se trunca a 3, para convertirlo en tipo int. En la segunda línea, tanto 1.6 como 1.7 se convierten en 1 por los operadores que los preceden, por lo que el valor asignado a rato nes será 2 en este caso. Como norma general, no se deben mezclar tipos. De hecho, hay muchos idiomas que lo prohíben. Existen ocasiones, sin embargo, en que puede re sultar útil. La filosofía del C se resume en evitar barreras innecesarias al usuario y otorgarle la responsabilidad de no abusar de su propia libertad.
125
Un programa ejemplo En la figura 5.7 hemos conseguido elaborar un programa útil (sobre to do, para los que practican footing a nivel internacional) que, además, sirve para revisar algunas de las ideas que se han expuesto a lo largo del capítulo. Parece largo, pero, en realidad, los cálculos se realizan en seis líneas hacia el final. El grueso del programa se dedica a una conversación del ordenador con el usuario. Se han introducido suficientes comentarios como para que el programa sea casi autoexplicativo, de manera que pasaremos a estudiarlo; más adelante aclararemos algunos detalles. /* footing */ #defíne SM 60 / * segundos por minuto*/ #define SH 3600 / * segundos por hora */ #define MK 0.62137 /* millas por kilómetro */
RESUMEN: OPERADORES EN C Se resumen aquí todos los operadores que hemos estudiado hasta ahora. I. Operador de asignación = Asigna el valor de su derecha a la variable situada a su izquierda. II. Operadores aritméticos + Suma el valor situado a su derecha y el situado a su izquierda. — Resta el valor situado a su derecha del situado a su izquierda. — Como operador unario, cambia el signo del valor situado a su derecha. * Multiplica el valor situado a su derecha por el situado a su izquierda. / Divide el valor situado a su izquierda entre el situado a su derecha. Si ambos valores son enteros, el resultado se trunca. % Calcula el resto de dividir el valor situado a su izquierda entre el situa do a su derecha (aplicable a enteros únicamente). + + Suma 1 a la variable situada a su derecha (modo prefijo) o a su izquier da (modo sufijo). -Igual que el anterior, pero restando 1. III. Miscelánea sizeof Entrega el valor, en bytes, del operando situado a su derecha. El ope rando puede se un especificador de tipo, en cuyo caso va entre pa réntesis —por ejemplo, sizeof (float)—, o una variable o array, que se usa sin paréntesis —sizeof pedro. (tipo) Operador de moldeado: Convierte el valor que le sigue en el tipo es pecificado por la(s) palabra(s) clave(s) colocada(s) entre los parénte sis. Por ejemplo, (float) 9 convierte el entero 9 en el número en pun to flotante 9.0.
main () { float distk, distm; /* distancia en kilómetros y en millas */ float veloc; / * velocidad promedio en millas hora */ int min, seg; /* minutos y segundos corriendo * / int tiempo; /*tiempo de carrera solo en segundos*/ float tporm; /* tiempo en segundos para una milla */ float mporm, sporm; /*minutos y segundos en una milla * / printf("Este programa convierte el tiempo de una carrera\n"); printf(“en tiempo para correr una milla y en promedio de\n"); printf("velocidad en millas por hora.\n"); printf(" Introduzca la distancia recorrida en km.\n”); scanf("%f", &distk); printf("Ahora indique el tiempo en minutos y segundos.\n”); printf("Comience por los minutos.\n"); scanf("%d", &min) ; printf("Y ahora los segundos.\n”); scanf("%c", &seg); tiempo = SM*min + seg; / * pasa el tiempo a segundos */ distm = MK*distk; /*pasa kilómetros a millas */ veloc = distm/tiempo*SH; / * millas por segundo multiplicado por segundos por hora = millas por hora */ tporm = (float) tiempo/distmi; /* tiempo por milla */ mporm = (int) tporm / SM; /* calcula minutos truncando */ sp o rm = (int) t p o r m % SM; / * calcula resto de segundos * / printf ("Ha corrido %1.2f km (%.1.2f millas) en %d min, %d seg\n"
}
, distk, distm, min, seg); printf ("Este ritmo corresponde a hacer 1 milla en %d min, ", mporm) ; printf ("%c seg. \nSu velocidad promedio fue %1.2f mph. \n", sporm, veloc); Figura 5.7
Un programa útil para corredores de fondo
Hemos empleado el mismo sistema que utilizamos en segamin para reali zar la conversión de tiempo final a minutos y segundos, pero también hemos usado conversiones de tipo. ¿Por qué? Porque necesitábamos argumentos en127
teros en la parte de programa dedicada a segundos y minutos, pero la con versión de kilómetros a millas hay que hacerla en punto flotante. Hemos usado el operador de “moldeado” explícitamente para realizar las conversiones. A decir verdad, también es posible escribir el programa haciendo conver siones automáticas. De hecho, así lo hemos planteado; hemos realizado otra versión en la que aprovechamos que tporm es de tipo entero para obligar a que el cálculo se transformase en un número entero. Sin embargo, esta ver sión sólo funcionó en uno de nuestros dos sistemas de referencia. Se com prueba así que el empleo explícito del operador hace que el programa sea más claro no sólo para el lector, sino también para el ordenador. Veamos un ejemplo de salida. Este programa convierte el tiempo de una carrera en tiempo para correr una milla y en promedio de velocidad en millas por hora. Introduzca la distancia recorrida en km. 10. 0 Ohora indique el tiempo en minutos y segundos. Comience por los minutos. 36 Y ahora los segundos. 23 Ha corrido 10.00 km (6.21 millas) en 36 min, 23 seg Este ritmo corresponde a hacer 1 milla en 5 min, 51 seg. Su velocidad promedio fue 10.25 mph.
b. x = (12 + 6)/2*3; C. y = x = (2 + 3)/ 4; d. y = 3 + 2* (x = 7/2); e. x = (int) 3.8 + 3.3;
2. Sospechamos que hay varios errores en el siguiente programa. ¿Podría localizarlos? main(){
int i = 1, float n; printf("Ojo, que va una ristra de fracciones!\n"); while (i < 30) n = 1/i; printf (" %f", n) ; printf("Eso es todo, amigos!\n");
} 3. Presentamos ahora el primer intento, realizado con la intención de hacer que se-
gamin sea interactivo. Este programa no es satisfactorio. ¿Por qué no? ¿Cómo po dría mejorarse? #define SM. 60 main()
{ int seg, min, resto;
Hasta ahora hemos aprendido Cómo utilizar algunos operadores: +, -, *, /, %, + + ,--, (tipo). Qué es un operando: aquello sobre lo que actúa el operador. Qué es una expresión: una combinación de operadores y operandos. Cómo se evalúa una expresión: siguiendo el orden de precedencia. Cómo se reconoce una sentencia: por sus puntos y coma. Algunas clases de sentencias: declaración, asignación, bucle while, com puestas. Cómo generar una sentencia compuesta: encerrando una serie de senten cias entre llaves { }. Cómo formar una sentencia while: while (test) sentencia. Qué sucede cuando se mezclan tipos distintos en la misma expresión: con versión automática.
Cuestiones y respuestas Cuestiones 1. Suponga que todas las variables son de tipo int. Indique el valor de cada una de
las siguientes variables. a. x = (2 + 3) * 6;
printf("Este programa convierte segundos a min y seg.\n"); printf("Introduzca el numero de segundos.\n"); while (seg > 0) { scanf("%d", &seg); min = seg/SM; resto = seg % SM; printf("%d seg son %d min, %d seg. \n", seg, min, resto); printf("Siguiente?\n”);
}
printf("Adios!\n");
}
Respuestas 1. a. 30 b. 27 (no 3). (12 + 6)/(2*3) sí que hubiese dado 3. c. x = 1, y = 1 (división entera) d. x = 3 (división entera) e y = 9 e. x = 6, ya que (int)3.8 = 3; 3 + 3.3 = 6.3, que se transforma en 6 al ser x entera. 2. La línea 3 debe terminar en punto y coma, no en coma. Línea 7: la sentencia while introduce en un bucle infinito, ya que i es menor de 30 y seguirá siéndolo siempre. Probablemente, lo que se quería indicar es while(i+ + < 30). Líneas 7-9: a juzgar por la indentación, se pretendía crear una sentencia compuesta con las líneas 8 y 9, pero, al no haber llaves, el bucle while afectará sólo a la sentencia 8. Deben incluirse, pues, llaves que abarquen estas dos sentencias. Línea 8: al ser enteros tanto 1 como i, la división realizada es entera, y el resultado será 1, 129
cuando i valga 1, y 0, para valores mayores. Si se desean sacar resultados distintos de 0 se deberá reescribir esta línea como 1.0/i, lo cual obliga a i a pasar a float (promoción) antes de efectuarse la operación. Linea 9: se ha omitido el carácter nueva línea ( \ n ) en la sentencia de control. Por tanto, todos los números se imprimirán en una sola línea, suponiendo que quepan. 3. El problema principal reside en la relación entre la sentencia que controla el bucle (¿es seg mayor que 0?) y la sentencia scanf( ), que captura el dato en segundos. En concreto, la primera vez que se realiza el test, el programa no ha tenido oportunidad siquiera de asig narle un valor a seg, por lo que la comparación se realizará con cualquier basura que por casualidad estuviera en esa dirección de memoria. Una posible solución, aunque terrible mente poco elegante, podría ser inicializar seg a, digamos, 1, para que disponga de un va lor con que ejecutar la primera pasada. Empero, esta solución (más bien parche) lo único que hace es destapar otro problema. En efecto, cuando se desee terminar el programa in troduciendo el valor 0, el bucle no se percata de ello hasta después de haber realizado la operación e imprimido el resultado de 0 segundos. Lo que necesitamos es una sentencia scanf( ) que esté colocada justo antes de la comparación del while. Observe la modifica ción de la parte central del programa que se ofrece a continuación: scanf("%d", &seg); while (seg > O) { min = seg/SM; resto = seg % SM; printf ("%d seg son %d min, %d seg. \n", seg, min, resto) ; print f("Siguiente?\n"); scanf ( "%/d ", &seg) ;
} La primera vez que entre el programa se empleará el valor sacado de la sentencia scanf( ) externa al bucle. El resto de valores se tomarán en el scanf( ) colocado al final del bucle, es decir, justo antes de la evaluación del while siguiente. Esta es una forma muy común de encarar problemas semejantes al expuesto aquí.
Ejercicios Presentamos ahora una serie de problemas para los que no se indica la respuesta. La forma de averiguar si su solución es correcta, o no, es teclearla en un ordenador y ejecutarla como programa. 1. Modifique nuestro programa, que calculaba la suma de los 20 primeros enteros (si lo prefiere, imagine que el programa calcula la cantidad de dinero que recibiría usted si le pagasen 1 el primer día, 2 el segundo, 3 el tercero, etc. Añada detrás la unidad monetaria que prefiera). La modificación a introducir consiste en que el programa pregunte interactivamente el límite superior del cálculo, es decir, sus tituya el número 20 por una variable a introducir desde teclado. 2. Modifique de nuevo el programa, de modo que calcule ahora la suma de los cua drados de números enteros (o, si lo prefiere, calcule cuánto dinero obtendría ga nando 1 el primer día, 4 el segundo, 9 el tercero, y así sucesivamente. Esperamos que se encuentre satisfecho con el cambio). En C no existe ninguna función para elevar al cuadrado, pero se puede utilizar la curiosa propiedad de que el cuadrado de un número n es precisamente n * n. 3. Modifique el programa una vez más, de tal forma que, cuando finalice un cálcu lo, solicite una nueva cantidad para repetir el proceso. Prepare una salida del pro grama cuando se introduzca como dato el 0. (Clave: utilice un bucle dentro de otro; vea también el problema 3 y su respuesta.) 30
6 Funciones de entrada/salida y reenvío En este capítulo encontrará: • E/S de un solo carácter: getchar ( ) y putchar ( ) • Buffers • Otra etapa • Lectura de una sola línea • Lectura de un fichero • Reenvío • UNIX • Reenvío de salidas • Reenvío de entradas • Reenvío combinado • Sistemas no UNIX • Comentario • E/S dependiente de sistema: puertos de E/S 8086/8088 • Utilización de un puerto • Resumen • Vamos a tantear la potencia oculta de nuestro ordenador • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios
133
Funciones de entrada/ salida y reenvío CONCEPTOS Entrada y salida (E/S) getchar( ) y putchar( ) Final de fichero (EOF, end-of-file) Reenvío: < y > E/S dependiente de sistema Bucles de retardo
Por otra parte, sería realmente un beneficio para todos si existiesen fun ciones E/S estándar en todos los sistemas; así se podrían escribir programas “transportables” que se adaptasen fácilmente de un sistema a otro. Existen en C muchas funciones E/S de este tipo, tales como printf( ) y scanf( ). Den tro de este tipo se incluyen también getchar( ) y putchar( ), funciones que estudiaremos a continuación. Estas dos funciones realizan la entrada y salida de un solo carácter a la vez. Al principio pudiera parecer una manera bastante estúpida de hacer las cosas; después de todo, tanto usted como yo somos capaces de leer fácilmen te agrupamientos mayores de un solo carácter. Sin embargo, este método se adapta mejor a las habilidades propias del ordenador; de hecho, constituye el núcleo central de la mayoría de programas que tratan con textos, es decir, con palabras ordinarias. Veremos a continuación cómo se desenvuelven estas dos funciones sencillas en programas que cuentan caracteres o leen y copian fi cheros. De paso, aprenderemos algo acerca de los buffers, ecos y reenvío.
E/S de un solo carácter: getchar( ) y putchar( ) La función getchar( ) toma un solo carácter (de ahí, su nombre) del te clado y lo entrega a un programa en ejecución. La función putchar( ), por su parte, toma una carácter de un programa en ejecución y lo envía a la pan-
Las palabras “entrada” y “salida” tienen más de un significado en la jerga informática. Podemos estar hablando de periféricos de entrada y salida, co mo teclados, unidades de disco, impresoras de matriz de punto, etc. Tam bién podemos referirnos a los datos que se están utilizando en entrada y sali da. Por último, podemos indicar con este término las funciones que llevan a cabo las entradas y salidas. En este capítulo nos dedicaremos principalmente a discutir estas funciones de entrada y salida (E/S, para abreviar), pero tam bién nos referiremos de cuando en cuando a los otros significados de E/S. Se llaman funciones de E/S a aquellas que transportan datos hacia y des de nuestro programa. Hasta ahora hemos utilizado dos funciones de este ti po: printf( ) y scanf( ). Nos dedicaremos ahora a contemplar algunas de las demás opciones que ofrece el C. Las funciones de entrada y salida no forman parte de la definición del C; su desarrollo queda a expensas de aquel que implemente el lenguaje en una determinada máquina. Si usted se dedica a crear un compilador C, po drá poner cualesquiera funciones de entrada/salida que prefiera. Si el siste ma para el que está diseñando el compilador tiene alguna característica espe cial, como la organización E/S de puertos del microprocesador 8086, podrá construir funciones E/S especiales que utilicen dicha característica. Veremos un ejemplo concreto de este sistema al final del capítulo. Figura 6.1
gerchar ( ) y putchar( ): los caballos de tiro en procesadores de textos 135
talla. Presentamos a continuación un ejemplo muy sencillo. Todo lo que hace es tomar un carácter del teclado e imprimirlo en pantalla. Iremos modificando poco a poco este programa hasta hacerlo de utilidad en una serie variada de aplicaciones. Ya iremos describiendo estas últimas más adelante; conténtese por ahora con nuestra humilde versión de comienzo.
putchar(’S’); /* observe que en constantes de */ putchar(’\n’); /* caracteres se usan apostrofos */ putchar(’\007’); putchar(ch); /* ch es una variable de tipo char */ putchar(getchar ());
Podemos utilizar este último ejemplo para reescribir nuestro programa como
/* getput1 */ #include (stdio.h) main()
{
#include (stdio.h) main()
char ch;
{ ch = getchar( ); putchar(ch);
/* /*
linea 1 */ linea 2 */
}
La mayoría de los sistemas contienen las definiciones de getchar y putchar en el fichero de sistema stdio.h; esa es la razón por la que hemos incluido dicho fichero en el programa. La ejecución de este programa produce salidas como:
putehar(getchar()) ;
} Esta es una forma totalmente compacta que no utiliza variables. Es más eficiente que la anterior, aunque quizá menos clara. Una vez visto cómo trabajan estas dos funciones, nos ocuparemos de los buffers.
Buffers
g [enter] g
o, posiblemente, como gg El símbolo [enter] es nuestra forma de indicar que se ha pulsado la tecla [enter]. En ambos casos la primera g ha sido la tecleada por usted, y la se gunda es la enviada por el ordenador. Que el resultado sea uno u otro depende de si su sistema tiene entrada con buffer o no. Si ha tenido que pulsar la tecla [enter] antes de obtener la respuesta, entonces su sistema tiene buffer. Nos dedicaremos por ahora a getchar( ) y putchar( ); posteriormente nos sumergiremos en el mundo de los buffers. La función getchar( ) carece de argumento (es decir, no hay nada entre los paréntesis). Simplemente captura el siguiente carácter y se otorga a sí misma el valor de dicho carácter. Por ejemplo, si captura la letra Q, la propia fun ción toma el valor Q. La línea 1 asigna entonces el valor de getchar( ) a la variable ch. Por el contrario, la función putchar( ) sí posee argumento. Entre los pa réntesis se deberá colocar aquel carácter que desee imprimir. El argumento puede ser un único carácter (incluyendo las secuencias de escape del capítu lo 3) o una variable o función cuyo valor sea un único carácter. Todos los ejemplos siguientes son usos válidos de putchar( );
Cuando ejecute este programa (cualquiera de las dos versiones), en algu nos sistemas puede darse el caso de que la letra introducida se repita inme diatamente. Se dice entonces que el ordenador está dando un “eco” de la entrada. En otros sistemas, por el contrario, no sucede nada hasta que se pulsa la tecla [enter]. El primer caso es un ejemplo de entrada "sin buffer" (o “di recta”), lo que significa que el carácter tecleado está disponible inmediata mente para el programa que ha detenido momentáneamente su ejecución. En el segundo caso, la entrada contiene un buffer que se suele considerar una especie de depósito, donde se almacenan los caracteres tecleados; allí irá a buscarlos el ordenador. La palabra buffer, común en el argot de ordenado res, hace referencia a una zona de memoria que se utiliza para almacenamiento temporal de datos, generalmente en entradas y salidas. Al pulsar la tecla [en ter] se da vía libre al programa para que capture el carácter o bloque de ca racteres que se haya introducido. En nuestro caso particular, únicamente se capturará el primer carácter, ya que esa es la misión de la función getchar( ). Por ejemplo, en un sistema con buffer se pueden obtener resultados como éste de nuestro programa anterior: He escrito esta linea. [enter] H
Por su parte, el sistema sin buffer devolvería la H tan pronto como se hubiese tecleado. La entrada-salida tendría un aspecto como: HHe escrito esta linea. 137
La segunda H procede del eco del programa. En cualquiera de los casos, el programa procesa únicamente un carácter, ya que se ha llamado a la fun ción getchar( ) una sola vez.
Otra etapa Vamos a intentar ahora algo más ambicioso que la lectura y escritura de un solo carácter. Supongamos que tenemos un gran número de caracteres. Deseamos que el programa se detenga en algún momento, de modo que asig naremos a un determinado carácter la orden de stop. En el ejemplo siguiente se utiliza un asterisco (*) con esta finalidad. La repetición continua del pro grama queda encomendada a un bucle while. /* getput2 */ /* Este programa captura e imprime caracteres hasta que se detiene */ #include #define STOP '*' /* Da a * el nombre simbolico STOP */ main()
{ char ch; ch = getchar(); while ( ch != STOP ) { putchar(ch); ch = getchar();
}
Figura 6.2
Entradas con y sin buffer
¿Por qué se utilizan buffers? En primer lugar, consumen mucho menos tiempo, ya que transmiten grupos de caracteres, en forma de bloques, en lu gar de enviarlos uno a uno. Además, si se equivoca al teclear, puede corregir con la tecla pertinente del teclado el error. Cuando se pulsa finalmente [enter], se transmitirá la versión correcta. Por otra parte, las entradas sin buffer pueden resultar adecuadas en algu nos programas interactivos. En un procesador de textos, por ejemplo, puede ser de utilidad que se ejecuten las órdenes en el momento en que se pulse la tecla correspondiente. Como ven, ambos sistemas tienen sus ventajas y su propio campo de aplicación. ¿Cuál tenemos nosotros? Se puede averiguar con facilidad ejecutando el programa anterior y observando el comportamiento de la salida en pantalla. En algunos compiladores C se ofrecen entradas y con sin buffer, a elección. En nuestro sistema, por ejemplo, existe la función getchar( ), para entrada con buffer, y getch( ), para entrada directa.
}
/* linea 9 */ /* linea 10*/ /* linea 1 1 * / /* linea 12*/
Hemos empleado aquí la estructura de programa que discutíamos en la cuestión 3 del capítulo 5. Al ejecutar por primera vez, putchar( ) toma su argumento de la línea 9; en las demás repeticiones se toma de la línea 12, hasta terminar el bucle. Hemos introducido también un nuevo operador de relación, ! = , que significa “no igual a”. Por tanto, la sentencia while per manecerá realizando bucles de lectura e impresión de caracteres hasta que el programa encuentre el carácter STOP. Podríamos haber omitido la sen tencia #define y emplear el * en la sentencia while, pero creemos que de esta forma el significado del programa es más obvio. En lugar de intentar ejecutar este maravilloso programa en su ordenador, eche un vistazo a la siguiente versión. Realiza exactamente la misma tarea, pero el estilo de la misma es más C. /* getput3 */ #include (stdio.h) #define STOP'*' main() { char ch;
}
while ( (ch = getchar() != STOP) /* linea 8 */ putchar(ch) ;
La línea 8 de esta maravilla sustituye a las líneas 9, 10 y 12 de getput2. ¿Cómo es posible? En principio ejecuta el contenido del paréntesis más interno: ch = getchar() 139
que corresponde a una expresión. Su efecto es activar getchar( ) y asignar su valor a ch. Con esto quedan sustituidas las líneas 9 y 12 de getput2. Para continuar, recuerde que una expresión tiene siempre un valor, y que una ex presión de asignación tiene el mismo valor global que la variable situada a la izquierda del signo =. Por tanto, el valor de (ch = getchar( )) es simple mente el valor de ch, y así (ch = getchar()) != STOP
tiene el mismo efecto que ch != STOP
Con esto se realiza la tarea que getput2 hacía en la línea 10. Este tipo de construcción (combinación de asignación y comparación) es muy común en C.
Pasemos ahora a un sistema con buffer. Esta vez no sucederá nada hasta que se pulse la tecla [enter]. Una posible salida podría ser: A ver si esto anda. * Humm, no lo se. [enter] A ver si esto anda.
Al programa se envía la primera línea completa. El programa lee enton ces el renglón a razón de un carácter por bucle, y los va imprimiendo hasta que encuentra el asterisco *. Hagamos ahora que el programa sea un poco más útil. Vamos a ir con tando los caracteres conforme se van leyendo; todo lo que tenemos que ha cer son unos pequeños cambios. /* cuentachl */ #define STOP * main()
{ char ch; int cuenta = 0
/*
inicializa cuenta de caracteres a 0
while ((ch = getchar( )) != STOP) { putchar(ch); cuenta++; /* suma 1 a cuenta
}
*/
*/
printf ( "\nHe leido un total de %d caracteres. \n" cuenta);
}
Figura 6.3
Examen de la condición en el bucle while
Se puede eliminar la línea putchar( ) si deseamos únicamente contar los caracteres sin recibir un eco de los mismos. Con un programa tan reducido como éste, y cambios menores, podemos disponer de programas que cuenten líneas y palabras. En el siguiente capítulo aprenderemos los detalles necesarios. Lectura de una sola línea
Al igual que en nuestro ejemplo anterior, cuando hacíamos while ( + + talla < 48.5), esta forma tiene la ventaja de colocar en el mismo lugar la condi ción de bucle y la acción que altera el índice del mismo. La estructura se ase meja bastante al proceso mental que puede seguir uno mismo: “Quiero leer un carácter, observarlo y decidir qué hacer a continuación.” Volvamos ahora al programa y ejecutémoslo. Si el sistema que está em pleando no tiene buffer, el resultado podría ser algo así: AA vveerr ssii eessttoo aannddaa..*Creo que si.
Todos los caracteres que aparecen en pantalla antes de la señal STOP (el asterisco) son reflejados en eco según se están tecleando. Incluso se duplican los espacios. Una vez pulsada la señal STOP, sin embargo, el programa se detiene, y lo que se escribe aparece en pantalla sin eco.
Entretanto, veamos qué otras mejoras podemos hacer con las herramien tas de que disponemos por el momento. Por ejemplo, se puede cambiar con facilidad la señal de stop. ¿Qué podemos emplear en lugar del asterisco para mejorar el sistema? Una posibilidad es utilizar el carácter nueva línea (\ n). Para ello, lo único que hay que hacer es redefinir STOP. #define STOP ’ \n’
¿Cuál será ahora el efecto obtenido? Bueno, el carácter nueva línea se transmite cuando se pulsa la tecla [enter], de modo que el programa así re dactado funcionará línea por línea en la entrada. Por ejemplo, supongamos que realizamos este cambio en cuentach1, y tecleamos la siguiente entrada: Que bonita es Marbella en verano,[enter] 141
La respuesta sería Que bonita es Marbella en verano, He leido un total de 33 caracteres.
En la sentencia printf( ) se ha incluido un \ n al comienzo y otro al final. La misión del primero es evitar que la respuesta del ordenador salga pegada a la coma del final del mensaje. El total indicado no incluye la tecla [enter], ya que el contador está situa do dentro del bucle. Disponemos ahora de un programa que lee una línea. Dependiendo de las sentencias que se incluyen dentro del bucle while, el programa puede ha cer un eco de la línea, contar los caracteres de la misma, o ambas cosas a la vez. Se empiezan a entrever ciertas aplicaciones para este programa, pro bablemente formando parte de un programa mayor. De todos modos, sería agradable disponer de un programa que leyese trozos mayores de texto, in cluso un fichero de datos completo. Para ello, lo único que se necesita es ele gir adecuadamente el carácter de STOP.
#define EOF (-1)
lo que permite utilizar expresiones como while ((ch = getchar()) != EOF)
en sus programas. Podemos, por tanto, reescribir nuestro programa básico de lectura y eco de la siguiente forma:
Lectura de un fichero
¿Cuál sería el STOP ideal? Debe ser algo que no aparezca normalmente en el texto; de esta forma se evita que el programa salte accidentalmente en la mitad de la entrada, deteniéndose antes de lo que nosotros hubiésemos de seado. Este tipo de problema no es nuevo, y, afortunadamente, está ya resuelto por los técnicos que diseñan sistemas de ordenadores. En realidad, su pro blema es ligeramente diferente, pero podemos utilizar la misma solución que ellos aplican. En su caso, el problema se concentra en el control de “fiche ros”. Se llama fichero a un bloque de memoria en el cual se almacena infor mación. Normalmente, un fichero se guarda en algún tipo de memoria per manente, como disco flexible, disco duro o cinta. Para saber dónde termina Prosa:
un fichero y comienza el siguiente resulta imprescindible disponer de un ca rácter especial que marque el fin de fichero. Necesitan, por tanto, un carác ter que no pueda aparecer en medio del fichero, al igual que nosotros necesi tábamos algo que no apareciese en medio de nuestra entrada. La solución es disponer de un carácter llamado Fin-De-Fichero, que se suele simbolizar como “EOF” (End-Of-File). La selección concreta del carácter EOF depen de del sistema; de hecho, puede consistir en más de un carácter; pero, en cual quier caso, dicho carácter existe, y su compilador C conoce cuál es el carác ter EOF de su propio sistema. ¿Cómo se puede utilizar un carácter EOF? Generalmente está definido en el fichero < stdio.h > . Una presentación bastante común es
El tiempo y el espacio no me hacen olvidar que soy humano
Prosa en un fichero:
El carácter EOF señala el final de fichero
Figura 6.4
Un fichero con EOF
/* getput4 * / #include main ()
{ int ch;
}
while ( (ch = getchar()) != EOF) putchar(ch);
Obsérvense los siguientes puntos: 1. No tenemos que denifir EOF, ya que de eso se encarga stdio.h. 2. No tenemos que preocuparnos del valor real del carácter EOF, ya que en la propia sentencia #define de stdio.h se usa la representación sim bólica EOF. 3. Hemos cambiado ch de tipo; ahora es de tipo int en lugar de tipo char. Lo hemos hecho porque char representa variables como enteros sin signo en el rango 0 a 255, en tanto que el “carácter” EOF puede tener el valor numérico —1. Tal valor sería imposible de alcanzar con una va riable char pero no con int. Por fortuna, getchar( ) es también de tipo int, de manera que puede leer el carácter EOF. 4. El hecho de que ch sea ahora un entero no altera la función putchar( ). Dicha función sigue imprimiendo los caracteres equivalentes. 5. Para utilizar este programa con entrada por teclado, necesitamos un sistema de enviar el carácter EOF. No, no sirve teclear las letras E-O143
F ni tampoco teclear —1 (—1 es el equivalente al código ASCII del carácter, no el propio carácter). En su lugar deberemos encontrar la tecla o combinación de teclas que emplee nuestro sistema en particu lar. La mayor parte de sistemas UNIX, por ejemplo, interpretan [control-d] (es decir, pulsar la tecla [d] manteniendo apretada la tecla [control]) como carácter EOF. Muchos microordenadores utilizan [control-z] con la misma finalidad. Un ejemplo de ejecución de getput4 en un sistema con buffer podría ser: Ya viene el cortejo! Ya viene el cortejo! Ya se oyen los claros clarines! Ya se oyen los claros clarines! Ruben Dario Ruben Dario [control-z]
Cada vez que se pulsa [enter], los caracteres almacenados en el buffer se procesan, y se obtiene una copia de la línea en pantalla. Esta acción continúa hasta que se teclea el carácter EOF. Detengámonos un momento y pensemos las posibilidades que puede ofrecer getput4. Este programa copia en pantalla cualquier cosa que se introduzca como entrada. Supongamos que conseguimos introducir un fichero en él. El
resultado sería que el contenido del fichero se imprimiría en pantalla, dete niéndose cuando se alcanzase el final del fichero, ya que ahí se encuentra un carácter EOF. Podemos suponer, por el contrario, que descubrimos una for ma de enviar salidas de programas a un fichero. Podríamos entonces teclear cualquier cosa en pantalla, y utilizar getput4 para almacenar lo que hemos tecleado. Supongamos, finalmente, que podemos realizar ambas operacio nes simultáneamente, es decir, obtener entradas de un fichero a getput4 y en viar las mismas como salida a otro fichero. En este caso, podríamos utilizar getput4 para copiar ficheros. Por consiguiente, nuestro pequeño programa es un potencial lector, creador y copiador de ficheros: ¡No está mal para su tamaño! La clave de todo el proceso consiste en controlar el flujo de entrada y salida, que es lo que trataremos a continuación.
Reenvío Como hemos dicho en un principio, en los procesos de entrada y salida se encuentran involucrados datos, funciones y periféricos. Sea un programa como getput4. En él se utiliza la función de entrada getchar( ). El periférico de entrada (suponemos) es un teclado, y los datos de entrada son caracteres individuales. Podría interesarnos mantener la misma función de entrada y el mismo tipo de datos, pero cambiar el lugar donde el programa se dirige a tomar los datos. Una buena pregunta a formular, y a responder, sería: “¿Có mo se las arregla un programa para saber dónde tiene que buscar su dato de entrada?” Por defecto, un programa en C se dirige siempre a la “entrada estándar” como fuente de entrada. Dicha “entrada estándar” puede ser cualquiera que haya sido dispuesta de esta forma para leer datos e introducirlos en el orde nador. Así, puede referirse a una cinta magnética, tarjetas perforadas, un teletipo, o, como supondremos en adelante, una terminal de video. Sin em bargo, un ordenador moderno es una herramienta bastante sugestionable, a la que se puede convencer con facilidad para que busque en otro sitio. En con creto, se puede indicar al programa que busque sus entradas en un fichero, en lugar de hacerlo en el teclado. Hay dos maneras de hacer que un programa trabaje con ficheros. La pri mera es utilizar funciones especiales que abran ficheros, o los cierren, lean, escriban, etc. Aún no estamos preparados para esta vía. Lo que sí podremos hacer es usar un sistema mucho más sencillo; se trata de utilizar un programa diseñado para trabajar con teclado y pantalla, pero “reenviando” las entra das y salidas por canales diferentes; por ejemplo, hacia y desde fichero. Esta segunda opción está más limitada que la primera en algunos aspectos, pero es mucho más sencilla de utilizar. El reenvío, también llamado redireccionamiento, es una característica del sistema operativo UNIX, no del C en sí mismo; sin embargo, resulta tan útil que, cuando se ha implementado C en otros sistemas, a menudo se ha inclui do en el paquete alguna forma de reenvío; además, existe el reenvío en mu chos de los sistemas operativos más modernos, incluyendo MS-DOS 2; por 145
Reenvío de entrada
tanto, aunque usted no sea usuario de un sistema UNIX, tiene bastantes po sibilidades de disponer de alguna forma de reenvío. Discutiremos, en princi pio, el reenvío en UNIX, y a continuación el reenvío en otros sistemas.
Supongamos ahora (esperamos que su máquina de suponer no se haya fundido todavía) que, en realidad, lo que deseamos es enviar nuestro texto a un fichero llamado mifrase. Para ello se debe ejecutar getput4 >mifrase
Reenvío de salidas Supongamos que hemos compilado nuestro programa getput4 y hemos colocado la versión ejecutable del mismo en un fichero llamado getput4. Pa ra ejecutar el programa, lo único que hay que hacer es teclear el nombre del fichero getput4
y el programa se ejecutará como se describió anteriormente, tomando la en trada desde teclado. Supongamos ahora que deseamos emplear el programa con un “fichero de texto’’ llamado palabra. (Se llama fichero de texto a aquel que contiene texto, es decir, datos almacenados en forma de caracteres. Pue de tratarse de un ensayo literario o de un programa C, por poner un ejem plo; sin embargo, un fichero que contiene instrucciones en lenguaje máqui na, como el que soporta la versión ejecutable del programa, no es un fichero de texto. Ya que nuestro programa trabaja con caracteres, debe ser utilizado en fichero de texto.) Para ello, introduciremos, en lugar de la anterior, la si guiente orden: getput4
El símbolo < es un operador UNIX de reenvío. Su misión es hacer que el contenido del fichero palabras se canalice hacia getput4. El programa getput4 ignora (o al menos no le importa) si la entrada de datos procede de un fiche ro o del teclado. Todo lo que sabe es que le están llegando una serie de carac teres, de modo que se limita a leerlos e imprimirlos uno a uno hasta que en cuentra un EOF. En UNIX, los ficheros y los periféricos de E/S se tratan de igual forma, por lo que nuestro fichero es ahora el “periférico’’ de E/S del sistema. ¡Inténtelo! getput4
Lo que desde luego no garantizamos es que en el fichero por usted em pleado surja, como en éste, Rosalía de Castro.
y a continuación comenzar a teclear. El símbolo > es otro operador UNIX de reenvío. Genera un nuevo fichero llamado mifrase y envía al mismo la salida de getput4 (tal salida es una copia de los caracteres que se teclean). Si ya existiese un fichero con el mismo nombre mifrase, normalmente se bo rrará y sustituirá por el actual. (Algunos sistemas UNIX permiten la opción de proteger ficheros ya existentes.) Lo único que aparece en pantalla son las letras tecleadas por usted, mientras que la copia se manda al fichero. Para terminar el programa, deberá introducir un carácter EOF, generalmente un [control-d] en sistemas UNIX. Haga un intento con esta nueva construcción. Si no se le ocurre nada que escribir, copie simplemente el ejemplo siguiente. En él se muestra una salida UNIX, en la cual suponemos que los comandos van precedidos por un %. No olvide separar las líneas con un [return], de manera que se pueda enviar el buffer al programa. getput4 >mifrase No tendra ningun problema en recordar cual es el operador de reenvio que entra y cual es el que sale. Recuerde simplemente que ambos apuntan en el sentido en que fluye la informacion. Piense en este sistema como si fuese un embudo. [control-d] %
%
Una vez que se detecta el [control-d], el programa finaliza y devuelve el control al sistema operativo UNIX. Esta operación queda indicada por la apa rición de otro nuevo símbolo %. ¿Cómo podemos saber que el programa ha funcionado? Existe un comando en UNIX, el ls, que produce un listado de los nombres de ficheros; si lo ejecuta, comprobará que existe ahora un nue vo fichero llamado mifrase. También se puede utilizar el UNIX cat para com probar el contenido, o bien usar de nuevo getput4, pero enviando esta vez , el fichero al programa. getput4
%
147
Comentaremos ahora las reglas que gobiernan el empleo de los dos ope radores de reenvío < y > .
Reenvío combinado
Aun a sabiendas de que corremos el riesgo de fundir definitivamente su neurona de suponer, imaginemos un nuevo caso. En esta ocasión deseamos hacer una copia del fichero mifrase, y llamarla guardafrase. Esta operación se realiza con el comando getput4 guardafrase
y nuestros deseos se ven cumplidos. El comando getput4 >guardafrase
es equivalente al anterior, ya que el orden en que se especifican las operacio nes de reenvío es indiferente. No emplee el mismo fichero como entrada y salida en un solo comando. getput4 mifrase ERROR
La razón por la que no se puede hacer es que > mifrase hace que el fichero mifrase original se borre incluso antes de ser utilizado como entrada.
1. Un operador de reenvío conecta un programa ejecutable, incluyendo comandos estándar UNIX, con un fichero. No pueden ser utilizados para conectar un fichero con otro o un programa con otro. 2. El nombre del fichero ejecutable debe estar a la izquierda del opera dor, y el nombre del fichero, a su derecha. 3. No se puede utilizar más de un fichero como entrada ni enviar la sali da a más de un fichero cuando se emplean estos operadores. 4. Normalmente, es opcional colocar espacios entre los nombres y los ope radores. Existen algunas excepciones cuando se utilizan algunos carac teres que tienen algún significado especial en el entorno UNIX. Así, po dríamos haber usado getput4 < palabras o getput4 < palabras, más elegante. Hasta ahora hemos dado ejemplos correctos. A continuación presenta mos algunos incorrectos, en los que se ha supuesto que suma y cuenta son programas ejecutables, y pez y estrella son ficheros de texto. pez > estrella suma < cuenta estrella > cuenta suma
> estrella
pez
Se Se Se Se Se
viola la regla viola la regla viola la regla viola la regla viola la regla
1 1 2 3 3
Existe también en UNIX el operador > > , que permite añadir datos al final de un fichero ya existente, y el operador “tubería” (pipe, |), que per mite conectar la salida de un programa con la entrada de un segundo progra ma. Si desea mayor información al respecto, consulte un libro de UNIX. (Hay uno excelente en esta misma colección.) En el siguiente ejemplo presentamos un programa muy sencillo de cifra do criptográfico. Si reformamos ligeramente getput, obtenemos: /* cod ig o */ /* este programa reemplaza cada caracter del texto */ /* por el siguiente en la secuencia ASCII */ #inclu de m a in () { int ch;
}
Figura 6.5
Reenvío combinado
while ( (ch = getchar()) != EOF) putchar(ch + 1);
La función putchar( ) convierte el entero “ch +1” en el carácter corres pondiente. 149
Compilemos el programa y almacenemos la versión ejecutable en un fi chero llamado código. A continuación, introduzcamos en otro fichero lla mado original el texto siguiente (empleando el editor del sistema o usando getput4, como antes): Para mejor comprensión se debe escribir sin errores.
observado, cinco de ellas contienen los símbolos < y > para reenvío. El reenvío general o por compilador se diferencia del reenvío UNIX en dos aspectos: 1. Funciona sólo con programas C, mientras que el reenvío UNIX fun ciona en cualquier programa ejecutable. 2. Se debe colocar un espacio entre el nombre del programa y el opera dor, y no puede existir espacio entre el operador y el nombre del fiche ro. Un ejemplo correcto de esta notación sería:
Si a continuación tecleamos el comando getput4
Comentarios el resultado podría ser algo así:
La P se ha transformado en una Q, la a en una b, etc. Observará también un par de detalles llamativos. Por ejemplo, los espacios han sido sustituidos por signos de admiración. Esto le recordará que el espacio es un carácter co mo los demás, por derecho propio. Además, las dos líneas se han transfor mado en una. ¿Por qué? Porque original contenía un carácter nueva línea al final de la primera línea; dicho carácter indica al ordenador que salte a la línea siguiente. Pero este carácter también ha sido alterado; en nuestro sis tema aparece en su lugar un ^K, que es otra forma de expresar [control-k], el cual, evidentemente, no hace comenzar una nueva línea. Si deseáramos un programa de cifrado que conservase la estructura de líneas original debería mos cambiar todos los caracteres menos el carácter nueva línea. En el siguiente capítulo encontraremos los comandos necesarios para ello.
El reenvío es una herramienta sencilla, pero potente. Permite transfor mar nuestro pequeño programa getput4 en un productor, lector y copiador de ficheros. Este enfoque es un ejemplo de la filosofía del C (y del UNIX) de creación de herramientas simples que pueden combinarse en multitud de formas para realizar tareas muy diferentes.
RESUMEN: COMO REENVIAR ENTRADAS Y SALIDAS La mayoría de sistemas C emplean reenvío, bien por estar incluidos dentro del sistema operativo, y ser válidos para cualquier programa, bien por estar implementados dentro del compilador C, y únicamente para programas C. En los ejemplos siguientes supondremos que prog es un programa ejecutable, y fich1 y fich2 son nombres de ficheros.
Reenvío de salida de un fichero: > Sistemas no UNIX
En este apartado haremos énfasis en las diferencias con el sistema UNIX, de manera que si usted se saltó la parte anterior le aconsejamos que retroce da y la lea. Hay dos variedades distintas dentro de los sistemas no UNIX: 1. Otros sistemas operativos con reenvío. 2. Compiladores C con reenvío. No pretendemos abarcar todos los posibles sistemas operativos; simple mente daremos un ejemplo, que está siendo ampliamente usado. Dicho siste ma es el MS-DOS 2. Este sistema comenzó siendo un vástago del CP/M, pe ro actualmente ha evolucionado hacia el XENIX, sistema semejante al UNIX. La versión 2 del MS-DOS 2 presenta operadores < y > , que funcionan exac tamente igual que los descritos en el apartado anterior. Tampoco podemos cubrir todos los posibles compiladores C. Sin embar go, de cada 6 versiones de compilador C para microordenador que hemos
prog >fich1
Reenvío de entrada desde un fichero: < prog
Reenvío combinado prog fich1 prog >fichl
Espaciado Algunos sistemas, especialmente los compiladores C, requieren que exista un espacio a la izquierda del operador de reenvío, y ninguno a la derecha. Otros sistemas, por ejemplo UNIX, aceptan cualquier combinación de espacios o ausencia de los mismos.
UN EJEMPLO GRAFICO
¿Qué se puede hacer con este programa? Bien, se puede ignorar por com pleto; también se puede intentar alterar, para conseguir salidas distintas. Se pueden también buscar combinaciones de caracteres que produzcan una salida agradable. Por ejemplo, los caracteres
Podemos utilizar getchar( ) y putchar( ) para producir diagramas geo métricos empleando caracteres. El siguiente programa realiza precisamente esto. En él se lee un carácter y se imprime repetidamente un cierto número de veces, dependiendo de su valor ASCII. También a la izquierda de la fila de caracteres se imprimen los espacios necesarios para que la línea quede centrada. /* diagramas */ / * produce un patron simetrico de caracteres */ #include main(){
int ch; /* lee caracter */ int indice; int numch; while ((ch = getchar()) != '\n') { numch = ch % 26; /* genera un numero entre 0 y 25 */ indice = 0;
while (indice++ < (30 - numch)) putchar(' '); /* espacios para centrar */ indice = 0; while (indice++ < (2*numch + 1)) putchar(ch); /* imprime ch varias veces * / putchar('\n'); }
} La única novedad técnica es que hemos empleado, dentro de las condi ciones de bucle while, subexpresiones como (30-numch). El control de los es pacios iniciales se realiza dentro de un bucle while, y la impresión de los caracteres, en un segundo bucle. La salida de este programa depende de la entrada. Por ejemplo, si tecleamos Que pasa?
la respuesta es
E/S dependiente de sistema: puerto de E/S 8086/8088 Como ejemplo de las posibilidades que tiene el C para adaptarse a los requerimientos de un sistema específico estudiaremos ahora un tipo diferen te de dispotivo de E/S. Muchos de los microordenadores de la nueva genera ción se basan en el chip microprocesador Intel 8086 y 8088. El ejemplo más conocido es el IBM PC, que utiliza el 8088. En nuestra exposición nos basa remos, en particular, en este ordenador, aunque los principios apuntados más abajo son aplicables a otros usuarios de la familia 8086/8088. Un ordenador como el IBM tiene bastantes cosas más que un chip 8088. Entre ellas, un teclado, un altavoz, una unidad de cassette o, quizá, de discos flexibles, un monitor, memoria, relojes y otros microprocesadores que con trolan el flujo de datos. La unidad central de proceso (incorporada dentro del chip 8088) necesita algún sistema para comunicarse con los demás compo nentes del ordenador. Algunas de estas comunicaciones se establecen utili zando direcciones de memoria, y otras usando “puertos” de entrada/salida. El chip 8088 tiene 65536 puertos utilizables en comunicaciones. Cada uno de los dispositivos externos posee su propio puerto o puertos de comunica ción con el 8088 (¡evidentemente, no se utilizan los 65536!). Por ejemplo, los puertos 992, 993 y 1000 al 1004 se emplean para comunicarse con el adap153
tador de gráficos/color. El altavoz se gobierna con el puerto 97. Esta segun da posibilidad suena bastante más sencilla que un adaptador de gráficos/co lor, de modo que usaremos el altavoz para ejemplarizar el empleo de los puertos E/S. El puerto 97 no controla directamente el altavoz. El dispotivo que lo hace es algo con el esotérico nombre de controlador Programable de Interface Pa ralelo 8255. Este microprocesador tiene tres “registros” (unidades de memo ria pequeñas y fácilmente accesibles), en los que puede almacenar números, a razón de uno en cada registro. Los números controlan lo que hace el dispo sitivo; cada registro está conectado al 8088 a través de un puerto, y el puerto 97, como podríamos imaginar, se conecta con el registro que controla el alta voz. De este modo, para programar el altavoz, podemos utilizar el puerto para cambiar el número contenido por el registro. Si acertamos con el número correcto, haremos que el altavoz emita un sonido. Por el contrario, si intro ducimos un número incorrecto podemos encontrarnos con serios problemas. Por tanto, es vital conocer qué número debemos enviar y cómo enviarlo; en nuestro caso particular debemos saber cómo se puede emplear el C para en viar dicho número. Nos ocuparemos, en principio, del número a enviar. Lo primero que hay que saber es que un registro 8255 acepta un número de 8 bits, que se almacena como número binario, como 01011011. Cada uno de los 8 bits almacenados se considera un conmutador sí-no para una determinada acción o un disposi tivo concreto. Así, la presencia de un 0 o un 1 en una disposición determina da indica si el dispositivo está conectado o no. Por ejemplo, el bit 3 (los bits se numeran de 0 a 7, comenzando por la derecha) determina si el motor del cassette está conectado, y el bit 7 activa y desactiva el teclado. Empezará a comprender la necesidad de obrar con precaución: si decidimos activar el altavoz, y olvidamos los demás bits, podemos encontrarnos con que el tecla do ha quedado accidentalmente anulado. En la siguiente figura se indica lo que hace cada bit. (La información está tomada de un manual técnico de re ferencia de IBM, y no es necesario que sepamos la mayor parte de los deta lles contenidos en él.)
Figura 6.7
Puerto 97: acciones controladas por cada bit
Observe los pequeños signos más y menos de la figura. El signo + signi fica que cuando el bit vale 1, se cumple la condición; el signo -, por contra, indica que el nivel activo es el 0. Así, un 1 en el bit 3 significa que el motor del cassette está desconectado, mientras que un 0 en el bit 4 indica que la memoria de lectura/escritura está activada. ¿Cómo conseguimos que funcione el altavoz? Por lo que se ve en la figura, parece que el altavoz (speaker, en inglés) está afectado por los bits 0 y 1. Po demos conectar el altavoz enviando el número binario 11 (equivalente al nú mero decimal 3) a través del puerto 97. Antes de intentarlo debemos perca tarnos de que esta acción tendría efectos colaterales, como hacer 0 el bit 4, que no es precisamente lo que deseamos que suceda. Por esta razón, no le hemos dicho todavía cómo se pueden usar los puertos. Para actuar con seguridad debemos comprobar, en primer lugar, cuál es el contenido del registro “en reposo’’. Afortunadamente, es bastante fácil enterarse (lo veremos en un momento). La respuesta es que el registro suele contener “76” ó “77”. Traslademos estos números a binario. (Quizá le con venga hacer una pequeña visita al apéndice de números binarios antes de con tinuar.) En la tabla 6-1 se muestra la conversión a binario de algunos núme ros decimales: Tabla 6-1. Conversión de algunos números decimales a binarios decimal
76 77 78 79
Figura 6.6
La conexión 8088-8255
número de bit 7
6
5
4
3
2
1
0
0 0 0 0
1 1 1 1
0 0 0 0
0 0 0 0
1 1 1 1
1 1 1 1
0 0 1 1
0 1 0 1
Sin entrar a averiguar qué puede significar algo como hold keyboard clock low, está claro que la política más conservadora es mantener todas las posi ciones de los bits intactas, con excepción de los bits 0 y 1. Esta acción es equi valente a enviar al registro el número binario 01001111, o decimal 79. Como precaución adicional, leeremos previamente el contenido del registro y lo de jaremos tal como estaba una vez que hayamos conseguido que el altavoz sue155
ne. (Las operaciones bit a bit discutidas en el apéndice presentan otra forma de asignar valores en el registro.) De acuerdo, ya estamos listos para hacer pitar al altavoz. ¿Y ahora qué? Utilización de un puerto
A través de un puerto se pueden hacer dos cosas. Se puede enviar infor mación desde el 8088 al dispositivo conectado a él o bien leer información del dispositivo y pasarla al 8088. Estas tareas se realizan en lenguaje ensam blador con las instrucciones OUT e IN. En C, el método a seguir depende del compilador; algunos compiladores ofrecen una función C análoga; por ejemplo, Lattice C y Supersoft C utilizan outp( ) e inp( ). En otros compila dores los nombres pueden ser ligeramente diferentes. Si el compilador que está utilizando no ofrece esta posibilidad, lo más probable es que pueda em plear lenguaje ensamblador para definir dicha función, o bien insertar sim plemente el código ensamblado directamente en su programa (una operación bastante sencilla). Eche un vistazo al manual de su compilador. Entretanto, supondremos que dispone de las funciones outp( ) e inp( ). He aquí un programa que hace pitar al ordenador. /* pito1 */ /* programa que hace sonar el altavoz */ main()
{ int loquehay; loquehay = inp(97); /* guarda valor inicial * / /* del puerto 97 */ printf ("puerto 97 = %d\n", loquehay); / * comprueba */ outp(97,79); /* envia 79 al puerto; conecta altavoz */ outp(97, loquehay); / * lo deja como estaba * /
}
Probablemente ya habrá inferido la misión de las funciones inp( ) y outp( ); por si acaso, ahí va una descripción más formal. inp(numero de puerto) Esta función devuelve un valor entero de 8 bits (que se convierte en un entero int de 16 bits aña diendo ceros a la izquierda) al puerto de entrada numero de puerto. En la operación no se produ ce alteración alguna en dicho puerto. outp(numero de puerto, valor) Esta función envía un valor entero de 8 bits al puerto de salida número de puerto.
Ejecutemos ahora el programa. Quizá se sienta un poco decepcionado, porque el ordenador desconecta el sonido casi inmediatamente después de producirse. Quedaría mucho más satisfactorio este programa si el ordenador se detuviese durante un instante antes de desconectar el altavoz de nuevo. ¿Cómo podemos hacerlo? Simplemente obligando al ordenador a ejecutar una tarea diferente entretanto. En el siguiente programa se consigue un piti do más largo. /* pito2 */ /* un pitido mas largo #define LIMITE 10000 main()
*/
{
int loquehay; int cont = 0; /* algo para contar
*/
loquehay = inp(97); outp (97, 79) ; while(cont++ < LIMITE) ; /* una sentencia que solo gasta tiempo */ outp(97, loquehay);
}
Observará que lo único que hace el bucle while es aumentar el valor del contador cont hasta llegar a LIMITE. El punto y coma que sigue al bucle while hace que éste ejecute una sentencia “nula”, es decir, una sentencia que no realiza ninguna acción. Por tanto, pito2 conecta el altavoz, cuenta hasta 10000 y, a continuación, lo desconecta. Se puede, evidentemente, ajustar el valor de LIMITE para controlar la duración del sonido. También se puede sustituir LIMITE por una variable, y controlar la duración por medio de va lores introducidos con scanf( ). Sería agradable controlar también el tono; de hecho es posible. Cuando acabe de estudiar estas funciones puede, si lo desea, leerse el apéndice, en que se presenta un programa que transforma el teclado de la terminal en un instrumento musical. Resumen
De nuevo nos hemos tropezado con dispositivos de E/S, datos de E/S y funciones de E/S. Los dispositivos han sido el controlador 8285 y el alta voz, los datos, los números comunicados desde y hacia uno de los registros del 8285 y las funciones, inp( ) y outp( ). Estas funciones, o sus equivalentes en código ensamblador, son necesarias para manejar los puertos E/S del 8086/8088; los compiladores C ofrecen generalmente una o las dos opciones.
Obsérvese que el puerto se puede utilizar como entrada y salida, depen diendo de la función aplicada.
157
Vamos a tantear la potencia oculta nuestro ordenador Lo que ignora seguramente es que bajo el teclado de inocente aspecto se esconde un brioso corcel, un verdadero purasangre. ¿Se atreve a domarlo? Hemos creado para ello un fabuloso programa (revelado en la figura 6.8), diseñado especialmente para la doma de su caballo. Es un programa que hay que ejecutar para valorarlo en lo que merece. Precaución: para conseguir el efecto adecuado deberá escoger un valor de LIMITE apropiado para su siste ma. Más adelante hablaremos de ello; por el momento, he aquí el programa.
la L final) para evitar problemas con el tamaño máximo int. Si se emplea el valor 8000, no hubiera sido necesario en el IBM PC; sin embargo, si cam biamos el valor a 12000, el número debe ser definido long, ya que la expre sión 3*LIMITE resulta 36000, que es mayor que el máximo int permitido en dicho sistema. Si su sistema no dispone de timbre o altavoz, intente cambiar la sentencia putchar(' \ 007') por printf(“CLOP \ n”). Con este programa conseguirá impresionar a sus amistades, y hará pro bablemente sonreír a aquellos que todavía temen a los ordenadores. Estamos convencidos de que el programa puede ser la base de una “cal culadora C”; dejamos el desarrollo de la idea a nuestro lectores.
/* Furia */ #include #define LIMITE 8000L main()
Hasta ahora hemos aprendido
{
int num1, num2; long retraso = 0; int cont = 0; printf("Furia, el caballo matematico, sumara dos\n"); printf("enteros no muy grandes para su divertimiento\n") ; printf("Introduzca el primer entero (que sea facil!)\n"); scanf( "%d", &num1) ; printf("Gracias. Introduzca el segundo\n"); scanf( "%d " , &num2) ; printf("Bien, Furia, cuanto suma eso?\n"); while( retraso++ < LIMITE); while( cont++ < (num1 + num2 - 1)) { putchar(' \007' ) ; retraso = 0; while( retraso++ < LIMITE);
Qué hace getchar( ): toma un carácter del teclado. Qué hace putchar(ch): envía el carácter ch a la pantalla. Qué significa != : no igual a. Qué es EOF: un carácter especial que indica el final de un fichero (End Of File). Cómo reenviar la entrada desde un fichero: programa < fichero. Cómo reenviar la salida a un fichero: programa > fichero. Qué son puertos: accesos de E/S a dispositivos conectados al micropro cesador. Cómo utilizar puertos: inp( ) y outp( ).
Cuestiones y respuestas
putchar('\n'); }
}
printf("Seguro?\n"); retraso = 0; while( retraso++ < 3*LIMITE); putchar('\007'); printf("Muy bien, Furia!\n"); Figura 6.8
Un programa devorador de números
Anotaciones técnicas: Las sentencias while que contienen retraso se limi tan a marcar tiempo. El punto y coma al final de la línea indica dónde acaba el bucle, el cual no incluye ninguna de las líneas siguientes. Cuando se usa un bucle while dentro de otro, la operación se denomina “anidado”. Hemos encontrado que el valor más apropiado para LIMITE en el IBM PC es 8000: para un VAX 11/750 preferimos un valor alrededor de 50000; en todo caso, en este segundo sistema la salida se puede ver afectada por el tiempo compar tido. Hemos hecho LIMITE igual a una constante long (ese es el origen de
Cuestiones 1. Sabemos que putchar(getchar( )) es una expresión válida. ¿Es también válida getchar(putchar( ))?
2. ¿Cuál es el resultado de la ejecución de las siguientes sentencias? a. putchar(‘H’); b. putchar(' \007'); c. putchar(' \n’); d. putchar(' \ b'); 3. Supongamos que tenemos un programa cuenta que cuenta los caracteres de un fichero. Preparar una orden que cuente el número de caracteres del fichero ensayo y almacene el resultado en un fichero llamado ensayoct. 4. Dado el programa y los ficheros de la cuestión anterior, ¿cuáles de los siguientes comandos serían válidos? a. ensayoct < ensayo b. cuenta ensayo c. cuenta < ensayoct d. ensayo > cuenta 5. ¿Cuál es el resultado de la sentencia outp(212,23)? 159
Respuestas 1. No. getchar( ) no utiliza argumento, mientras que putchar( ) necesita uno. 2. a. imprime la letra H. b. envía a la salida el carácter ‘\007’, que produce un pitido. c. comienza una nueva línea. d. retrocede un espacio. 3. cuenta < ensayo > ensayoct o bien cuenta > ensayoct < ensayo. 4. a. inválido, ya que ensayoct no es un programa ejecutable. b. inválido, por haberse omitido el operador de reenvío. (Sin embargo, aprenderemos más adelante a escribir programas que no necesitan dicho operador.) c. válido, da como resultado el número de caracteres en el mensaje producido por cuenta en la cuestión 3. d. inválido; el nombre del programa ejecutable debe aparecer en primer lugar. 5. Envía el número 23 al puerto 212.
Ejercicios 1. Escriba un programa como el descrito en la cuestión 3; es decir, un programa que cuente el número de caracteres de un fichero. 2. Modifique cuenta de manera que emita un sonido cada vez que cuenta u n carác ter. Incluya un bucle de retraso para separar un pitido del siguiente. 3. Modifique pito2 de manera que pueda introducirse como dato el límite de conta je del bucle cuando se ejecute el programa.
160
7 Una encrucijada en el camino En este capítulo encontrará: • La sentencia if • La sentencia if con else • Elección: if-else • Elección múltiple: else-if • Cada else con su if • ¿Quién es el más grande?: operadores de relación y expresione: • ¿Qué es la Verdad? • ¿Y qué más es Verdad? • Problemas con las verdades y las mentiras • Prioridad de operadores de relación • Seamos lógicos • Prioridades • Orden de evaluación • Un programa para contar palabras • Una caricatura con caracteres • Análisis del programa • Longitud de las líneas • Estructura del programa • Disposición de los datos • Comprobación de errores • El operador condicional: ?: • Elección múltiple: switch y break • Hasta ahora hemos aprendido • Cuestiones y respuestas
163
Una encrucijada en el camino
La primera forma ya la conocemos de sobra: todos nuestros programas, hasta ahora, han consistido en secuencias de sentencias. Disponemos tam bién de un ejemplo de la segunda forma, el bucle while, y completaremos este apartado en el capítulo 8. El último punto, elección entre diferentes ma neras posibles de actuar, hace los programas mucho más “inteligentes” y aumenta enormemente la utilidad de un ordenador. A ello dedicaremos este capítulo.
CONCEPTOS Toma de decisiones Qué es verdadero y falso en C Cómo hacer comparaciones Lógica en C
PALABRAS CLAVE if, else, switch, break, case, default OPERADORES
La sentencia if Comenzaremos con un ejemplo muy simple. Ya hemos visto cómo escri bir un programa que cuenta el número de caracteres de un fichero. Suponga mos que, en su lugar, deseamos contar líneas. Para ello debemos contar el número de caracteres nueva línea que aparecen en el fichero. Esta operación se realiza así: /* cuentalineas */ #include main()
{
>> = < = < = =! =&& || ! ?:
int ch; int numlin = 0;
while(( ch = getchar()) != EOF) if (ch =='\n')
}
numlin++; printf("He contado %d lineas.\n");
El núcleo de este programa es la sentencia i f (ch == '\n') numl in++;
Cuando se estudia un lenguaje de programación, uno sueña con crear pro gramas poderosos, inteligentes, versátiles y útiles. Para ello necesitamos un lenguaje que disponga de las tres formas básicas de control de “flujo” del programa. De acuerdo con las ciencias del cómputo (ciencia del cómputo es aquella que estudia los ordenadores, no la ciencia hecha por ordenadores: al menos, por el momento), un buen lenguaje de programación debe presen tar las siguientes formas de flujo de programa: 1. Ejecución de una serie de sentencias. 2. Repetición de una secuencia de sentencias hasta que se cumpla una de terminada condición. 3. Empleo de un test para decidir entre acciones alternativas.
Esta “sentencia if" indica al ordenador que aumente numlin en 1 cuando el carácter leído (ch) es el carácter nueva línea. El símbolo == no es un error de imprenta: significa “es igual a”. No confunda este operador con el opera dor de asignación ( = ). ¿Qué sucede si ch no es igual a un carácter nueva línea? Nada. El bucle while avanza para leer el siguiente carácter. La sentencia if que acabamos de emplear es a todos los efectos una única sentencia, que se extiende desde el if inicial hasta el punto y coma final. Por ello no tenemos que utilizar llaves para marcar los límites del bucle while. Es bastante sencillo hacer un programa que cuente a la vez caracteres y líneas; vayamos con ello.
165
/ * ccl-contador de caracteres y lineas # include
printf("Bingo!\n"); /* sentencia simple
*/
main() {
int ch;
if
int numlin = 0; int numcar = 0; while(( ch = getchar()) != EOF)
*/
(joe > bob) { pastajoe++; printf("Perdiste, forastero.\n"); } /* sentencia compuesta */
{
numcar++; if (ch == '\n' ) numlin++;
}
}
printf ("He contado %d caracteres y %d lineas. \n", numcar, numlin);
La forma sencilla de una sentencia “if” permite elegir entre ejecutar una sentencia (simple o compuesta) o saltarla. En C se permite también la elec ción entre dos sentencias empleando la estructura if-else. Elección: if-else
Ahora el bucle while contiene dos sentencias, de manera que hemos colo cado llaves para marcar el comienzo y final del mismo. Podemos llamar al programa compilado ccl y utilizar un operador de reen vío para contar los caracteres y líneas de un fichero llamado poe. ccl
El siguiente objetivo dentro del desarrollo de este programa es conseguir que cuente palabras. Esta opción es un poco más complicada que lo realiza do hasta ahora. Antes de abordarla necesitamos saber algo más sobre las sen tencias if.
En el último capítulo hemos presentado un programa muy sencillo de co dificación en clave, que convierte cada carácter en el siguiente en la secuen cia ASCII. Por desgracia, también ha quedado convertido el carácter nueva línea, con lo que se ha acumulado el texto en una sola línea en la “traduc ción”. Se puede eliminar este problema creando un programa que haga una elección muy sencilla: si el carácter es nueva línea, dejarlo como está; si no, convertirlo. En C se realiza esta elección de la siguiente forma: /* codigo1 */
#include main()
{
char ch; while ( (ch = getchar()) != EOF) { if (ch == '\n') /* deja caracter nueva */ putchar(ch); /* linea sin alterar */
La sentencia if con else La forma más simple de una sentencia if es la que acabamos de utilizar: if (expresion) sentencia
La expresión incluida en la sentencia es, generalmente, de relación, es de cir, una expresión que compara el tamaño de dos cantidades (x > y o c == 6, por ejemplo). Si la expresión es cierta (x es mayor que y, o c es igual a 6, se ejecuta la sentencia que va a continuación. Si es falsa, la sentencia se igno ra. Generalizando aún más, se puede emplear cualquier expresión, y si ésta tiene un valor 0 se toma como falsa; ampliaremos este punto más adelante. La porción que corresponde a la sentencia puede ser una sentencia simple como en nuestro ejemplo, o una sentencia compuesta (o bloque) delimitada por llaves:
el se
}
}
putchar(ch + 1 ) ; /* cambia los demas */
La vez anterior empleamos un fichero que contenía el siguiente texto: Para mejor comprensión se debe escribir sin errores.
Si utilizamos el mismo texto con nuestro nuevo programa, el resultado será ahora: !!!!!Qbsb!nfkps!dpnqsfot j po t f!efcf!ftdsjejs!t jo!fsspsft/
167
¡Caramba! Parece que funciona. Por cierto, se puede hacer un programa decodificador muy sencillamente: basta con duplicar codigo1, sustituyendo (ch + 1) por (ch — 1). ¿Ha observado la forma general de la sentencia if-else? Es if (expresion) sentencia else sentencia
Si la expresión es cierta, se ejecuta la primera sentencia; si es falsa se eje cuta la sentencia que está colocada a continuación de else. Las sentencias pue den ser simples o compuestas. Como ya hemos dicho varias veces, no se ne cesita indentación en C, pero la forma presentada arriba es bastante están dar. Con ella se pueden observar de un vistazo las sentencias cuya ejecución depende de un test. La sentencia if permite escoger entre realizar una acción o no. Con if-else se puede escoger entre dos acciones diferentes. ¿Qué sucede si deseáramos tener más de dos alternativas?
cho. Lo veremos en un ejemplo concreto. Las compañías de suministro a me nudo cargan en los recibos tarifas que dependen de la cantidad gastada. Su pongamos que las tarifas por consumo eléctrico son: los primeros 240 kwh: los siguientes 300 kwh: por encima de 540 kwh:
5.418 ptas. por kwh 7.047 ptas. por kwh 9.164 ptas. por kwh
Como sabemos que está deseando descifrar su recibo de la luz, vamos a prepararle un programa que le permita calcular sus costos. El siguiente ejem plo es un primer intento de este programa. /* reciboluz */ calcula el recibo de la luz * / #define TARIFA1 5.418 / * tarifa de los primeros 240 kwh */ #define TARIFA2 7.047 /* tarifa de los siguientes300 kwh */ #define TARIFA3 9.164 / * tarifa por encima de 540 kwh */ #define BASE1 1300.0 /* coste total primeros240 kwh */ #define BASE2 3414.0 /* coste total primeros540 kwh */ #define LIMITE1 240.0 / * primer bloque de tarifa */ #define LIMITE2 540.0/* segundo bloque de tarifa */ /*
Elección múltiple: else-if
main() {
En la vida aparecen frecuentemente más de dos posibles alternativas. Po demos ampliar la estructura if-else con else-if, para acomodarnos a este he-
float kwh; /* kilowatios gastados */ float recibo; / * precio */ printf ( " Introduzca el gasto en kwh. \n") ; scanf("%f", &kwh) ; if (kwh < LIMITE1) recibo = TARIFA1 * kwh; else if (kwh < LIMITE2) /* kwh entre 240 y 540 * / recibo = BASE1 + TARIFA2 * (kwh - 240); else / * kwh por encima de 540 * / recibo = BASE2 + TARIFA3 * (kwh - 540); printf("La cuenta total por %. 1f kwh es %.0 pts.\n", kwh, recibo);
}
Figura 7.1
if e if-else
Hemos empleado constantes simbólicas para las tarifas; de este modo nues tras constantes están reunidas en un solo sitio. Si la compañía cambia las ta rifas (lo que, por desgracia, sucede con demasiada frecuencia), el hecho de disponer de todas ellas en el mismo lugar hace más sencilla la modificación del programa. También hemos utilizado símbolos para los límites de tarifas; dichos límites pueden, asimismo, ser modificados eventualmente. El flujo del programa es completamente directo, seleccionándose una de las tres fórmu las dependiendo del valor de kwh: en la figura 7.2 se ejemplifica dicho flujo. Debemos aclarar que la única posibilidad de que el programa alcance el pri mer else es que kwh sea igual o mayor que 240. Por tanto, la línea else if (kwh < LIMITE2) equivale realmente a averiguar si kwh está comprendido entre 240 y 540, como se advierte en el comentario del programa. De igual 169
forma, el else final sólo puede alcanzarse si kwh es mayor o igual que 540 Por último, observe que BASE1 y BASE2 representan el cargo total por los primeros 240 y 540 kwh, respectivamente. Para un cálculo de gastos mayo res, lo único que tenemos que hacer es calcular el cargo adicional por la elec tricidad consumida en exceso respecto a estas cantidades.
como otras son ignorados por el compilador. De cualquier manera, se pre fiere la primera forma, que muestra más claramente que estamos eligiendo entre tres posibles alternativas. Esta forma hace más fácil la revisión del pro grama y la comprensión de las alternativas de que se trata. La forma anida da, por el contrario, resultará útil cuando se deseen comparar dos cantidades diferentes. Un ejemplo aplicable a nuestro programa podría ser si se estable ciese un 10 por 100 de recargo en los kwh que excedieran de 540, únicamente durante el verano. Se pueden unir tantos else-if cuantos se deseen, como se comprueba en el fragmento siguiente: if (tanteo < 1000) bonus = 0; else íf (tanteo < 1500) bonus = 1; else if (tanteo < 2000) bonus = 2; else if (tan teo < 25 00) bonus = 4; else bonus = 6;
Este fragmento podría ser parte de un programa de juego, en donde bo nus representa cuántas bombas de fotones adicionales o paquetes de comida recibirá usted en la siguiente ronda. Figura 7.2
Flujo deI programa reciboluz
En realidad, la construcción else-if es simplemente una variación de lo que ya sabíamos. Por ejemplo, el núcleo del programa anterior se podría ha ber escrito también
else
prin tf("L o sien to, h as pe rdido!\n");
kwh < LIMITE2)
re cib o = B AS E 1 + else re cib o = B AS E 2 +
Cuando se encuentran un gran número de if y else reunidos, se podría uno preguntar cómo decide el ordenador el if que corresponde a cada else. Por ejemplo, consideremos el siguiente trozo de programa: if ( numero > 6 ) íf ( numero < 12 ) prin t f("C alien te!\n"); else
if (kwh < LIMITE1) r e c i b o = TARIFA1 * k w h ; if (
Cada else con su if
TARIFA2 *
(kwh - 240);
TARIFA3 *
(kwh - 540);
Es decir, el programa consiste en una sentencia if-else, en la cual la parte de sentencia else es, a su vez, otra sentencia if-else. Se dice que la segunda sentencia if-else está “anidada” en la primera. (Por cierto, toda la estructura if-else contabiliza como una sola sentencia, por lo que no hay necesidad de encerrar el if-else anidado entre llaves.) Las dos formas son completamente equivalentes. Las únicas diferencias están en los lugares en que colocamos los espacios y las líneas; tanto unos
¿Cuándo se escribirá: “!Lo siento, has perdido!”? ¿Cuando número sea menor o igual que 6 o cuando sea mayor que 12? Dicho de otra forma, ¿el else va con el primer if o con el segundo? La respuesta es que va con el segundo if. Es decir, en la ejecución de este programa obtendría una salida como ésta: Número Respuesta 5 ninguna 10
Caliente!
15
Lo siento, has perdido!
171
La regla a observar es que el else va con el if más próximo, a menos que haya llaves que indiquen lo contrario. Hemos indentado nuestro programa haciendo aparentar que else iba con el primer if; recuerde, sin embargo, que el compilador ignora la indentación. Si realmente desease que else fuese con el primer if, habría que reescribir el fragmento de la siguiente manera: if ( numero > 6 )
{ if ( numero < 12 ) printf("Calíente!\n");
} else printf("Lo siento, has perdido!\n");
RESUMEN: UTILIZACION DE SENTENCIAS if PARA ELEGIR ALTERNATIVAS Palabras clave: if, else Comentarios generales: En cada una de las formas siguientes la sentencia puede ser simple o compues ta. Se considera “verdadera”, en general, cualquier expresión cuyo valor sea distinto de 0. Forma 1: if ( expresión ) sentencia
La sentencia se ejecuta si la expresión es cierta. Ahora se obtienen las siguientes respuestas: Número Respuesta 5 Caliente! 10 Lo siento, has perdido! 15 ninguna
Forma 2: if ( expresión ) sentencia]
else sentencia2 Si la expresión es cierta, se ejecuta sentencia1. Si es falsa, se ejecuta sentencia2.
Forma 3: if ( expresión1 ) sentencia1
else if ( expresión2 ) sentencia2
else sentencia3
Si expresión1 es cierta, se ejecuta sentencia1. Si expresión1 es falsa, pero expresión2 es cierta, se ejecuta sentencia2. Si ambas son falsas, se ejecuta sentencia3. Ejemplo:
Figura 7.3
Apareando if con else
if (patas == 4) printf("Debe ser un caballo. \n") ; else if (patas > 4) printf("No es un caballo.\n") ; else /* casos de patas < 4 */
{ patas++; printf("Ahora tiene una pata mas.\n");
}
173
Los operadores de relación se utilizan para formar las expresiones em pleadas en sentencias if y while. Estas sentencias comprueban si la expresión es cierta o falsa. Los cuatro ejemplos siguientes contienen sentencias de rela ción cuyo significado, esperamos, está bastante claro.
Quien es el más grande: operadores de relación y expresiones Los operadores de relación se emplean para hacer comparaciones. Ya he mos utilizado algunos, y lo que sigue es una lista completa de estos operado res en C. OPERADOR
< <= == > = > !=
if ( numero < 6) prin tf("E l nu m ero de be se r m a yor.\n ");
while ( ch != ’$’)
SIGNIFICADO
cont++;
es menor que es menor o igual que es igual a es mayor o igual que es mayor que es distinto de
if (total == 100) prin tf(”H a co nsegu ido u n plen o!.\n ");
if < ch > ’ M’ ) prin tf("E nviar este sujeto a otra linea .\n");
Con esto quedan cubiertas todas las posibilidades de relaciones numéri cas. (Los números, aunque algunos de ellos sean complejos, son bastante me nos complejos que los humanos.) Se debe poner especial cuidado en no con fundir = por = = . Algunos lenguajes de ordenador (por ejemplo, BASIC) utilizan el mismo símbolo para el operador de asignación y para el operador de relación de igualdad, pero las dos operaciones son completamente dife rentes. El operador de asignación asigna un valor a la variable situada a su izquierda; sin embargo, el operador de relación de igualdad comprueba si sus partes izquierdas y derechas son ya iguales. En ningún caso se cambia el valor de la variable de la izquierda, suponiendo que haya alguna.
Obsérvese que las expresiones de relación se pueden también utilizar con caracteres. Para la comparación se emplea código máquina (que hemos esta do suponiendo ASCII). Por el contrario, los operadores de relación no sir ven para comparar tiras de caracteres; en el capítulo 13 se muestra cómo pro ceder con estas últimas. De igual forma, los operadores de relación se pueden utilizar con núme ros en punto flotante. Sin embargo, en estos números se aconseja emplear únicamente comparaciones < y >. La razón es que dos números pueden no ser iguales debido a errores de redondeo, aunque lógicamente debieran serlo. Imaginemos, por ejemplo, este ejemplo equivalente en decimal. Si mul-
canoas = 3 asigna el valor 3 a canoas canoas = = 5 comprueba si canoas tiene el valor 5
Cualquier precaución es poca a este respecto, ya que el compilador pue de, en ocasiones, permitirle utilizar la forma errónea, dando resultados que pueden ir desde una simple broma hasta una auténtica catástrofe. Más ade lante veremos un ejemplo.
COMPARACION = = COMPRUEBA SI EL VALOR DE CANOAS ES 5
Tabla 7-1: Operadores de asignación y de relación de igualdad en algunos lenguajes comunes Lenguaje
Asignación
Relación de igualdad
BASIC FORTRAN C PASCAL PL/I LOGO
= = = := = make
= .EQ. == = = =
ASIGNACION
= ASIGNA A CANOAS EL VALOR 3
F igu ra 7.4
175
tiplicamos 3 por 1/3, el resultado debe ser 1.0; pero si escribimos 1/3 como número con 6 cifras decimales, el producto es .999999, que no es lo suficien temente igual a 1. Cada expresión de relación se enjuicia como “cierta” o “falsa”. Este punto presenta implicaciones de gran interés. ¿Qué es la Verdad?
Esta pregunta se la han formulado filósofos de todas las epocas. Noso tros nos daremos el gusto de contestarla, al menos en lo que respecta al C. En primer lugar, recuerde que cada expresión en C siempre tiene un valor. Esto es cierto incluso para expresiones de relación, tal como se demuestra en el siguiente ejemplo. En él calculamos los valores de dos expresiones, una cierta y una falsa.
cia if elegirá el primer camino en la bifurcación (la sentencia detrás de if) mientras que el segundo if tomará el camino alternativo (la sentencia detrás de else). Ejecute el programa y compruebe si estamos en lo cierto. ¿Y qué más es verdad?
Hemos utilizado un 1 y un 0 como expresión de la sentencia if; ¿podemos emplear otros números? Si lo hacemos, ¿qué sucedería? Experimentemos. /* test de if */ mai n()
{
if(200)
}
printf("200 es cierto.\n"); if(-33) printf(”-33 es cierto.\n");
/* ciertoyfalso */ main()
Los resultados son:
{
200 es cierto. -33 es cierto.
int cierto, falso;
}
cierto = ( 10 > 2 ); /* valor de una relacion cierta */ falso = ( 10 == 2); /* valor de una relacion falsa */ printf ( "cierto = %d; falso = %d \n", cierto, falso);
Aparentemente, el C toma 200 y —33 como “cierto” también. De hecho, cualquier valor distinto de 0 será “cierto”, y únicamente se toma como “fal so” 0. ¡Realmente, este lenguaje tiene una noción de la Verdad muy tolerante!
Aquí hemos asignado los valores de dos expresiones de relación a dos va riables. Para ser consecuentes, hemos asignado cierto al valor de una expre sión cierta, y falso al valor de una falsa. La ejecución del programa produce el siguiente resultado: cierto = 1; falso = 0
¡Ajá! Para el C, la Verdad es 1, y la Falsedad es 0. Podemos comprobar lo fácilmente con el siguiente programa.. /* test de la verdad */ main () {
if (1) printf("1 significa cierto. \n"); else printf("1 no significa cierto.\n"); if (0)
}
printf("0 no significa cierto.\n">; else printf("0 significa cierto.\n");
La suposición de partida es que 1 se evaluará como sentencia cierta, y 0 como sentencia falsa. Si lo que suponemos es correcto, la primera senten 177
Muchos programadores hacen uso de esta definición de verdad. Por ejem plo, la frase if(cabras != 0)
Pero, ¿qué ha sucedido? Aparte de que el diseño del programa deja bas tante que desear, hemos olvidado el aviso de prevención que comentábamos un poco más atrás, empleando if (edad = 65)
puede sustituirse por
en lugar de
if (cabras)
ya que la expresión (cabras != 0) y la expresión (cabras) se hacen 0 o falsas si, y sólo si, cabras tiene el valor 0. Por nuestra parte, pensamos que la se gunda forma no tiene un significado tan claro como la primera. Sin embar go, es más eficiente, ya que el ordenador necesita hacer menos operaciones cuando se ejecuta el programa. Problemas con las verdades y las mentiras
Esta “manga ancha” que muestra el C para reconocer la verdad puede crear problemas. Consideremos el siguiente programa. /* empleo */ main() { int edad = 20; while (edad++ <= 65) { if ((edad % 20) == 0) /* edad divisible por 20? */ printf("Ha cumplido %d. Le subimos el sueldo. \n", edad); if (edad = 65) printf ("Ha cumplido %d. Aqui esta su reloj de oro. \n", edad); } }
if (edad == 65)
El efecto, como se puede ver, es desastroso. Cuando el programa alcanza esa línea toma la expresión (edad = 65). Como expresión de asignación que es, hace que la variable tome el valor 65. Al ser 65 distinto de 0, la expresión se declara “cierta”, y se ejecuta la siguiente instrucción de impresión. A con tinuación el programa vuelve al test del bucle while, con edad valiendo 65, lo cual es menor o igual a 65. Al cumplirse la condición del test, edad se in crementa a 66 (debido a la notación sufija del operador incremento + +), y se ejecuta el bucle de nuevo. ¿Por qué no se detiene ahora? Debería hacer lo, ya que edad es ahora mayor que 65. Pero, ¡ay!, cuando el programa al canza nuestra sentencia errónea de nuevo, edad recupera el valor 65 otra vez. Así el mensaje se imprime una vez más y el bucle se repite ad infinitum. (A menos, por supuesto, que usted decida detener el programa o desenchufar el ordenador.) En resumen, empleamos los operadores de relación para formar expre siones. Las expresiones de relación tienen valor “1”, si son ciertas, y “0”, si son falsas. Las sentencias que emplean normalmente expresiones de rela ción como test (por ejemplo, while e if) pueden usar, en realidad, cualquier expresión; si su valor es distinto de 0, se tomará como “cierta”, y si es 0, como “falsa”.
A primera vista se podría pensar que la salida de este programa sería Prioridad de las operaciones de relación Ha cumplido 40. Le subimos el sueldo. Ha cumplido 60. Le subimos el sueldo. Ha cumplido 65. Aqui esta su reloj de oro.
Sin embargo, la salida real Ha Ha Ha Ha Ha Ha
cumplido 65. cumplido 65. cumplido 65. cumplido 65. cumplido 65. cumplido 65.
Aqui Aqui Aqui Aqui Aqui Aqui
es estasu esta su esta su esta su esta su esta su
El nivel de preferencia de los operadores de relación es menor que el de + y —, y mayor que el del operador de asignación. Por ejemplo, esto signi fica que x > y + 2
reloj reloj reloj reloj reloj reloj
de de de de de de
oro. oro. oro. oro. oro. oro.
es lo mismo que x > (y + 2)
También significa que ch = getchar() != EOF
y así indefinidamente. 179
es lo mismo que ch = (getchar( ) != EOF)
ya que la mayor prioridad de != indica que dicha operación se realiza antes de la asignación. Así pues, ch tendrá el valor 1 ó 0, ya que (getchar( ) != EOF) es una expresión de relación cuyo valor se asigna a ch. Comprenderá ahora por qué utilizábamos paréntesis en los programas de ejemplos anteriores, en los que deseábamos que ch tomase el valor de getchar( ):
> !=
es mayor que es distinto de
II. Expresiones de relación: Una expresión de relación simple consiste en una operación de relación con un operando a cada lado. Si la relación es cierta, la expresión toma el valor 1. Si es falsa, toma el valor 0. III. Ejemplos: 5 > 2 es cierta y tiene el valor 1 (2 + a) == a es falsa y tiene el valor 0
(ch = getchar( )) != EOF
Los propios operadores de relación están organizados en dos categorías diferentes: grupo con mayor prioridad: < < = = > > grupo con menor prioridad: == != Al igual que la mayoría del resto de operadores, éstos también asocian de izquierda a derecha. Así, alfa != beta == gamma
Seamos lógicos Algunas veces es útil combinar dos o más expresiones de relación. Por ejemplo, supongamos que deseamos escribir un programa que cuente única mente los caracteres que no sean espacios en blanco. Es decir, deseamos con tar los caracteres que no son espacios, ni caracteres nueva línea, ni caracteres tabulado. Empleamos operadores “lógicos” para cumplir este requerimien to. El siguiente programa es un ejemplo del método a seguir.
es lo mismo que /* cuentacar */ (alfa != beta) == gamma
En C se comprueba, en primer término, si alfa y beta son iguales. El va lor resultante, 1 ó 0 (cierto o falso), se compara a continuación con el valor de gamma. En realidad, este tipo de expresiones no se emplean normalmen te, pero creemos que es nuestro deber informar de su existencia. Recordamos al lector/a que desee mantener claras sus prioridades que en el apéndice C hay una tabla completa de todos los operadores ordenados por rango.
RESUMEN: OPERADORES DE RELACION Y EXPRESIONES I. Operadores de relación: Todos estos operadores comparan el valor a su izquierda con el valor a su de recha. < <= == >=
es menor que es menor o igual que es igual a es mayor o igual que
/* cuenta caracteres no blancos */ main()
{ int ch; int numcar = 0;
while ((ch = getchar( ) != EOF) if ( ch != ' ' && ch != '\n' && ch != '\t')
numcar++; }
printf("He contado caracteres no blancos. \n" numcar) ;
La ejecución comienza como en otros muchos programas anteriores: se lee un carácter y se comprueba si es el carácter fin de fichero (EOF). A conti nuación aparece algo nuevo, una sentencia que utiliza el operador lógico “y” (and), &&. La sentencia if que lo contiene se puede interpretar de la siguiente manera: Si el carácter no es un blanco Y, no es un carácter nueva línea Y, no es un caracter de tabulado, aumenta numcar en 1. Para que la expresión completa sea cierta, lo deben ser las tres condicio nes por separado. Los operadores lógicos tienen una prioridad menor que los operadores de relación, de manera que no es necesario emplear paréntesis adicionales para agrupar las subexpresiones. 181
Orden de evaluación
Existen tres operadores lógicos en C: OPERADOR
SIGNIFICADO
&& || !
and (y) or (o) not (no)
Supongamos que exp1 y exp2 son dos expresiones de relación simples, como gato > rata o deuda = = 1000. En ese caso: 1. exp1 && exp2 es cierto sólo si tanto exp1 como exp2 son ciertas. 2. exp1 || exp2 es cierta si lo son exp1, o exp2, o ambas. 3. ¡exp1 es cierta si exp1 es falsa, y viceversa. Veamos algunos ejemplos concretos:
Normalmente, en C no se garantiza qué parte de una expresión compleja se evalúa primero. Por ejemplo, en la sentencia manzanas =
(5
+ 3) * (9 +
6)
;
la expresión 5 + 3 podría evaluarse antes de 9 + 6, o podría hacerlo des pués; sin embargo, la precedencia de los operadores sí garantiza que ambas serán evaluadas antes de que se realice la multiplicación. Esta ambigüedad, como ya se ha comentado, se dejó intencionadamente en el lenguaje, a fin de permitir que los diseñadores de compiladores pudiesen preparar versiones más eficientes para su sistema particular. No obstante, hay una excepción a esta regla (más bien una falta de regla), concretamente en el tratamiento de operadores lógicos. En C se garantiza que las expresiones lógicas se eva lúan de izquierda a derecha. También queda garantizado que tan pronto se encuentre un elemento que invalida la expresión completa cesa la evaluación de la misma. Con estas garantías se pueden emplear construcciones como
5 > 2 && 4 > 7 es falsa porque sólo una de las dos subexpresiones es cierta. 5 > 2 || 4 > 7 es cierta porque al menos una de las subexpresiones es cierta. !(4 > 7) es cierta porque 4 no es mayor que 7. La última expresión, por cierto, es equivalente a
4 <= 7 Si los operadores lógicos no le son familiares, o se encuentra incómodo con ellos, recuerde que
while ((c
=
getchar( ))
!=
EOF && c
!= ' \ n ' )
La primera subexpresión asigna un valor a c, el cual debe utilizarse en la segunda subexpresión. Si no se hubiese garantizado el orden, el ordenador podría haber intentado evaluar la segunda expresión antes de encontrar el valor de c. Otro ejemplo podría ser if ( numero != 0 & & 12/numero == 2) printf("El numero es 5 o 6.\n");
practica && tiempo == perfección
Prioridades El operador ! tiene una prioridad muy alta, mayor que la multiplicación, igual a la de los operadores incremento e inmediatamente inferior a la de los paréntesis. El operador && tiene mayor prioridad que || estando ambos si tuados por debajo de los operadores de relación y por encima de la asigna ción. Por consiguiente, la expresión
Los operadores lógicos emplean usualmente expresiones de relación como operandos. El operador ! utiliza un solo operando. El resto usa dos, uno a la iz quierda y otro a la derecha.
se interpretará como &&
( b > c ) )
RESUMEN: OPERADORES LOGICOS Y EXPRESIONES I. Operadores lógicos:
a > b & & b > c | | b > d
( ( a > b )
Si número tiene un valor 0, la expresión es falsa, y el resto de la misma no se evalúa. Así se le evita al ordenador el trauma de intentar una división por 0. Muchos lenguajes no poseen esta característica; después de compro bar que el número es 0, intentan todavía averiguar el resultado de la siguien te condición.
| |
( b
>
d )
es decir, b está comprendido entre a y c, o b es mayor que d.
&& || !
and (y) or (o) not (no) 183
{
II.
palabra = SI; np++;
Expresiones lógicas:
expresión1 && expresión2 es cierta si, y sólo si, ambas expresiones son cier
tas. expresión1 || expresión2 es cierta si una de ellas o ambas son ciertas. !expresión es cierta si la expresión es falsa, y viceversa. III.
Orden de evaluación:
Las expresiones lógicas se evalúan de izquierda a derecha; la evaluación se de tiene tan pronto se descubre algo que hace falsa la expresión total. IV.
Ejemplos
6 > 2 && 3 = = 3 ! ( 6 > 2 && 3 = = 3 ) x != 0 && 20/x < 5
es cierta es falsa sólo se evalúa la segunda expresión si x es distinto de cero.
Aplicaremos ahora nuestros nuevos conocimientos a un par de ejemplos. El primero nos recordará programas ya vistos.
Programa para contar palabras Disponemos ahora de todas las herramientas necesarias para escribir un programa que cuente palabras (y de paso contar también caracteres y líneas, si lo deseamos). El punto clave es buscar una manera de enseñar al ordenador a distinguir palabras. Tomaremos un camino relativamente sencillo, definiendo una palabra como una secuencia de caracteres sin espacios en blanco. Por tanto, “glymxck” y “r2d2” son palabras. Emplearemos una variable llama da palabra, que nos indicará si estamos o no en una. Cuando encontremos un espacio en blanco (un espacio, tabulado o nueva línea) reconoceremos que se ha alcanzado el final de una palabra. En ese momento, el próximo carác ter no blanco localizado se considerará el comienzo de una nueva palabra, y se incrementará el contador correspondiente en 1. El programa es el siguiente: #include #define SI 1 #define NO 0 main() { int ch; /* para capturar caracteres long nc = 0L; /* numero de caracteres int nl = 0; /* numero de lineas int np = 0; /* numero de palabras int palabra = NO; / * == SI si ch esta en una palabra */
while ((ch = getchar( )) != EOF)
{
nc++;
/* cuenta caracteres */
if (ch == '\n')
nl++; /* cuenta lineas */ if (ch != ' ' && ch != '\n' && ch != '\t' && palabra == NO)
}
if ( (ch == ' ' || ch == '\n' || ch || ' \t' ) && palabra == SI) palabra = NO; / * final de palabra */
} printf("caracteres = %ld, palabras = %d, lineas = %d\n",
}
nc, np, nl) ;
Hemos tenido que emplear operadores lógicos para comprobar los tres posibles tipos de caracteres en blanco que podíamos encontrar. Considere mos, por ejemplo, la línea if (ch != ' ' && ch != ' \n' && ch != ' \t'
&& palabra == NO)
que se leería “si ch no es un espacio, y no es una nueva línea, y no es un tabulado, y no estamos en una palabra”. (Las tres primeras condiciones jun tas están preguntando si ch no es un espacio en blanco.) Cuando se cumplen las cuatro condiciones a la vez, debemos estar comenzando una nueva pala bra; por tanto, se incrementa np; por el contrario, si estamos en mitad de una palabra, se cumplen las tres primeras condiciones, pero palabra será SI, y np no se incrementa. Cuando se alcance el siguiente carácter espacio en blan co, haremos palabra igual a NO de nuevo. Estudie el programa, para comprobar si es capaz de contar palabras aun cuando se incluya más de un carácter en blanco entre dos consecutivas. Si desea usar el programa con un fichero, utilice reenvíos.
Una caricatura con caracteres
*/
Ocupémonos ahora de algo menos utilitarista y más decorativo. Nos pro ponemos crear un programa que sea capaz de dibujar figuras compuestas por caracteres. Cada línea de la salida se compondrá de una fila de caracteres única, es decir, sin interrupciones. El programa deberá permitirnos decidir el carácter, así como la longitud y posición de la fila. El programa aceptará datos hasta que lea un carácter EOF. En la figura 7.5 presentamos el listado. Supongamos que llamamos al programa ejecutable monigotes. Para eje cutar dicho programa, teclearemos su nombre. A continuación introducire mos un carácter y dos números; el programa responderá; entonces se intro ducirá un nuevo grupo de datos, al que seguirá una nueva respuesta del pro grama, y así sucesivamente hasta que se introduzca un carácter EOF. En un sistema UNIX este intercambio podría ser; 185
% monigotes B 10 20 BBBBBBBBBBB
Y 12 18 YYYYYYY [control-d]
%
/* Monigotes */ /* este programa dibuja figuras rellenas de caracteres */ #include ) #define LONGMAX 80 main()
{ int ch; int princ, int cont;
/* caracter a imprimir */ final;/* puntos de comienzo y final*/ /* contador de posicion */
: : : : : : : /
31 30 29 27 25 30 30 30 : 35 : 35 48
49 49 49 49 49 49 49 49 48
Si ejecutamos ahora el comando monigotes < fig, la salida obtenida es la que se puede observar en la figura 7.6.
while((ch = getchar( )) != EOF) /* lee un caracter */
{ if (ch != '\n') /* salta caracter nueva linea */
{
scanf("%d %d", &princ, &final); /* lee limites */ if (princ > final || princ < 1 | | final > LONGMAX) printf("Limites no validos.\n”); else { cont = 0; while (++cont < princ) putchar(' '); /* imprime blancos hasta comienzo */ while
Figura 7.6
Una salida del programa de caricatura Figura 7.5
Programa de caricaturas
Como se observa, el programa ha impreso el carácter B en las columnas 10 a 20, y el carácter Y en las columnas 12 a 18. Por desgracia, si se emplea el programa interactivamente de esta manera, las órdenes se entremezclan con las salidas. Si deseamos que el dibujo no tenga interferencias, podemos crear previamente un fichero que contenga los datos, y emplearlo en reenvío como datos para el programa. Supongamos, por ejemplo, que hemos creado un fichero fig con los datos siguientes:
(Nota: La relación vertical a horizontal en los caracteres es diferente en impresoras y pantalla. Esto produce que figuras como la anterior aparezcan comprimidas en la vertical, cuando se imprimen, en comparación con las ob tenidas en una pantalla.) Análisis del programa
Este programa es corto, pero bastante más complicado que los ejemplos dados anteriormente. Observemos con detalle algunos de sus elementos. Longitud de las líneas
_ | | | | | =
30 30 30 30 30 30 20
50 50 50 50 50 50 60
Hemos limitado el programa para que no se pueda escribir más allá de la columna 80, ya que la anchura estándar de la mayor parte de monitores de vídeo y de las impresoras de tamaño normal es precisamente 80 caracte res. Sin embargo, puede redefinir el valor de LONGMAX si desea usar el programa en un periférico de mayor longitud de línea. 187
Estructura del programa
El programa se compone de tres bucles while, una sentencia if y otra if. else. Veamos lo que hace cada una. while ( (ch = getchar( )) != EOF)
El propósito del primer bucle while es permitirnos leer varios conjuntos) de datos. (Cada conjunto consiste en un carácter y dos enteros que indican el comienzo y final de línea.) Se lee el carácter primero, lo que permite com binar la lectura con una comprobación de fin de fichero (EOF). Cuando se encuentra un carácter EOF, el programa se detiene sin leer valores de princ y final. En los demás casos, se leen dos valores para princ y final, respectiva mente, por medio de scanf( ), valores que son procesados a continuación; con esto se completa el bucle. Después se lee un nuevo carácter y se repite el proceso. Observe que utilizamos dos sentencias, no una, para leer los datos. ¿Por qué no hemos empleado una sola? scanf("%c %d %d", &ch, &princ, &final);
Supongamos que se hubiera hecho así. Imagine lo que sucedería cuando el programa acaba de leer la última línea de datos de un fichero. Cuando el bucle comience de nuevo, lo único que quedará en el fichero será el carác ter EOF. La función scanf( ) leerá dicho carácter y lo asignará a ch. A conti nuación intentará leer un valor para princ, pero ya no hay nada en el fichero. Entonces el ordenador musita una queja, y el programa muere ignominiosa mente. Por el contrario, si separamos la lectura del resto del carácter, damos al ordenador una oportunidad para comprobar el EOF antes de intentar leer algo más. if ( c h ! = ' \ n ' )
El propósito de la primera sentencia if es, simplemente, hacer más senci lla la entrada de datos. Explicaremos cómo funciona en la siguiente sección. if (princ > final || princ < 1 || final > LONGMAX) printf("Limites no validos.\n”); else
La sentencia if-else está colocada con la intención de evitar que el progra ma juegue con valores peligrosos de princ y final. También tratamos este punto en la siguiente sección; no obstante, observe que hemos empleado operado res lógicos y de relación para investigar la eventual existencia de tres posibles peligros.
El cuerpo principal del programa está formado por la sentencia compuesta que sigue a else cont = 0;
En primer lugar, tenemos un contador que igualamos a 0. while (++cont < princ) putchar(' ');
A continuación comienza un bucle while que imprime espacios en blanco hasta la posición princ; por ejemplo, si princ es 10, se imprimen 9 espacios; por tanto, el carácter empieza a imprimirse en la columna 10. Observe que empleamos la forma prefijo del operador incremento junto con el operador < para conseguir este resultado. Si hubiésemos usado cont + + < princ, la comparación se habría realizado antes de incrementarse cont, imprimien do un espacio adicional. while ( cont++ <= final) putchar(ch);
El segundo bucle while de este bloque está dedicado a imprimir el carác ter desde la columna princ a la columna final. Esta vez hemos usado la for ma sufija y el operador < =. Esta combinación produce el efecto deseado 189
de imprimir el carácter hasta la posición final inclusive. Puede comprobarlo fácilmente por ensayo y error. putchar('\n');
Finalmente, se utiliza putchar(‘ \ n’) para acabar la línea y comenzar una nueva. Disposición de los datos
Dedicamos este apartado a una cuestión importante: la interacción entre los datos de entrada y el programa que se está ejecutando. Es éste un punto a considerar cuando se escribe un programa determinado. Los datos empleados en la entrada deberán tener una forma compatible con las funciones de entrada utilizadas por el programa. La introducción de datos de forma correcta es responsabilidad del usuario, por lo menos en un programa sencillo. Un programa más sofisticado, sin embargo, debe inten tar cargar con parte de la responsabilidad de esta introducción. En nuestro caso, la forma más clara para introducir los datos es: H 10 40 I
9
41
es decir, el carácter seguido por las posiciones de las columnas de comienzo y final. Pero nuestro programa también acepta esta forma:
H
10 40 I
9 41 o ésta:
La función getchar( ) lee el primer carácter que encuentra, sea alfabéti co, un espacio, una nueva línea, o cualquier otra cosa. La función scanf( ) hace exactamente lo mismo si se emplea el formato %c (carácter). Sin em bargo, cuando se usa scanf( ) con formato %d (entero), ésta ignora los espa cios y los caracteres nueva línea. Por ello, no hay problemas en colocar cual quier número de espacios o caracteres nueva línea entre el carácter leído con getchar( ) y el siguiente entero leído por scanf( ). Además, scanf( ) lee ci fras hasta que encuentra un carácter que no sea numérico; por ejemplo, un espacio, un carácter nueva línea o un carácter alfabético. De ahí que necesi temos un espacio o nueva línea entre el primero y el segundo entero, de for ma que scanf( ) pueda advertir que se ha acabado de escribir el primer nú mero y comienza el siguiente. Con esto queda explicado por qué debemos dejar un espacio o un carác ter nueva línea entre un carácter y el entero siguiente o entre los dos enteros. Pero, ¿por qué no podemos colocar caracteres de este tipo entre el último entero de un conjunto de datos y el siguiente carácter? La razón es la siguiente: cuando se recomienza de nuevo el bucle while, getchar( ) actuará exactamente allá donde scanf( ) acabó; por tanto, leerá justamente el carácter siguiente al entero leído, aunque sea un espacio, un carácter nueva línea o cualquier otro. Si tuviéramos que seguir al pie de la letra las demandas de getchar( ), de beríamos preparar una estructura de datos como la siguiente: hlO 50a20 60yl0 30
sin dejar ninguna separación entre el segundo entero de cada grupo y el si guiente carácter; pero la apariencia de esta disposición de datos es franca mente horrible, y hace que el 50 parezca que pertenece a la a, y no a la h. Por eso colocamos también la línea if (ch != '\n')
que hace que el programa salte en el caso de que ch sea un carácter nueva línea. Este último if nos permite usar
H 10 40I 9 41
pero no ésta:
h10 50 a20 60 y10 30
H 10 40 I 9 41
¿Por qué hay espacios opcionales y otros que no lo son? ¿Por qué puede haber un carácter nueva línea, pero no un espacio entre el último entero de un conjunto de datos y el primer carácter del siguiente? Estas cuestiones van más allá de los propios límites del programa. Para contestarlas, deberemos repasar el funcionamiento de getchar( ) y scanf( ).
en lugar de la disposición anterior, con un carácter nueva línea entre el 50 y la a. El programa lee el carácter nueva línea, no hace nada con él y a conti nuación busca un nuevo carácter. Comprobación de errores
Hemos considerado también el problema de tropezamos con un usuario perverso o simplemente desconocedor del funcionamiento del programa. La 191
cuestión es que se debe realizar un control de los datos de entrada antes de permitir que el ordenador trabaje con ellos. Una de las técnicas usadas en este sentido es la de “comprobación de errores”. Se trata de que el ordena dor compruebe el dato y decida si es aceptable dentro de su contexto. Una iniciativa hacia este objetivo serían las dos líneas incluidas en el programa: if (princ > final || princ <1 || final > LONGMAX) printf("Limites no validos.\n") ;
que forman parte de una estructura if-else que indica que la parte principal del programa se ejecutará sólo cuando todos los test if sean falsos. ¿Contra qué nos hemos protegido? En primer lugar, no tiene sentido que la posición de comienzo sea mayor que la posición final; los terminales im primen normalmente de izquierda a derecha, y no en sentido contrario. La expresión princ > final comprueba este posible error. En segundo lugar, la primera columna de una pantalla es la columna 1; no se puede escribir a la izquierda del margen izquierdo; la expresión princ < 1 nos preserva del error subsiguiente. Finalmente, la expresión final > LONGMAX comprueba que nos hemos pasado del margen derecho. ¿Hay alguna otra posible fuente de error? ¿Podemos dar otros valores erróneos a princ y final? Bien, siendo muy retorcidos, podríamos intentar que princ fuese mayor que LONGMAX. ¿Pasaría este valor nuestro test? No. Es cierto que no comprobamos este error directamente; sin embargo, supon gamos que princ es mayor que LONGMAX. En ese caso, final sería también mayor que LONGMAX (en cuyo caso cazamos el error) o bien sería menor que LONGMAX. Pero si final es menor que LONGMAX, también será me nor que princ, en cuyo caso atrapamos el error en el primer test. Otra posible fuente de error sería que final fuese menor que 1. Dejamos al lector que com pruebe que este error tampoco “cuela”. La parte de programa dedicada al test es muy simple. Si en alguna oca sión diseña un programa para una aplicación seria, deberá dedicar más es fuerzo a esta parte concreta. Por ejemplo, convendría que en los mensajes de error se identificara qué valor o valores son erróneos y por qué. Además, puede proyectar su propia personalidad sobre los mensajes. Algunas posibi lidades serían: El valor 897654 de FINAL es algo superior al limite de pantalla. Que cosas! El valor PRINC es mayor que FINAL. Use otro, por favor EL VALOR DE COMIENZO HA DE SER MAYOR DE 0, ESTUPIDO.
La redacción concreta del mensaje es cuestión de gustos, por lo que la dejamos al usuario.
Operador condicional: ?:
un operador en dos partes que contiene tres operandos. El siguiente ejemplo calcula el valor absoluto de un número: x=(y<0) ? -y : y;
La expresión condicional abarca la porción de sentencia entre el signo = y el punto y coma. El significado de la sentencia es el siguiente: si y es menor que 0 entonces x y; si no lo es, x y. Expresado en forma if-else sería:
,
=-
=
if (y < O) x = -y;
else x = y;
La forma general de la expresión condicional es expresión1 ? expresión2
: expresión3 Si expresión1 es cierta (distinta de 0), la expresión condicional total toma el valor de la expresión2; si expresión1 es falsa (0), toma el valor de la expresión3. Se puede utilizar la expresión condicional cuando se tiene una variable que puede tomar dos valores posibles. Un ejemplo típico es hacer una varia ble igual al mayor de dos valores: max = (a > b) ? a : b;
En realidad, las expresiones condicionales no son necesarias, ya que se puede ejecutar la misma tarea con sentencias if-else; sin embargo, son más com pactas, y generan usualmente códigos en lenguaje máquina más compactos.
RESUMEN: EL OPERADOR CONDICIONAL I. El operador condicional: ?: Este operador tiene tres operandos, cada uno de los cuales es una expresión. Se organizan de la siguiente forma: expresiónl ? expresión2 : expresión3. El valor de la expresión total es igual al valor de la expresión2, si expresión1 es cierta, mientras que si es falsa toma el valor de la expresión3. II. Ejemplos: (5 > 3 ) ? 1 : 2 (3 > 5 ) ? 1 : 2 (a > b ) ? a : b
tomael valor 1 toma el valor 2 toma el valor mayor entre a y b
El C ofrece una forma abreviada de expresar la sentencia if-else. Se deno mina “expresión condicional” y emplea el operador condicional ?:. Es éste 193
Nos sentimos un poco perezosos y nos detuvimos en la “e”. Veamos có mo funciona en ejecución antes de pasar a explicar sus distintas partes
Elección múltiple: switch y break Tanto el operador condicional como la construcción if-else permiten incluir en un programa elecciones entre dos alternativas con gran facilidad. En ocasiones, sin embargo, necesitamos un programa que elija una entre varias alternativas. Ya hemos visto que se puede realizar este tipo de elección con una cadena if-else if—. . . —else; pero en la mayoría de los casos es más conveniente emplear la sentencia switch que ofrece el C. Se presenta a continuación un ejemplo que demuestra cómo funciona. Este programa lee una letra, y responde imprimiendo el nombre de un animal que comienza con di cha letra. /* animales */ main()
{
Deme una letra y respondere con un nombre de animial que comience por ella. Pulse una letra; para terminar pulse #. a [return] aranillo, oveja salvaje del Caribe Introduzca otra letra o un #. d [return] destemplat, pinguino rojo de Kenia Introduzca otra letra o un #. r [return] Humm. ... ese no me lo se Introduzca otra letra o un #. Q [return] Solo me trato con letras minusculas Introduzca otra letra o un #. # [return]
char ch; printf("Deme una letra y respondere con "); printf("un nombre de animal\nque comience por ella.\n"); printf("Pulse una letra; para terminar pulse #. \n"); while((ch = getchar()) != '#')
{
if (ch != '\n')
{
/*
salta caracter nueva linea
if (ch >= 'a' && ch <= 'z') switch (ch)
/*
*/
solo minusculas
*/
{
case 'a' : printf("aranillo, oveja salvaje del Caribe\n"); break; case 'b' :
printf("babirusa, cerdo salvaje de Malasia\n"); break; case 'c' : printf("chascalote, ballena gigante del Amazonas\n" ) ;
break; case 'd' : printf("destemplado, pinguino rojo de Kenia\n"); break; case 'e' : printf("equigobo, camello siberiano\n"); break; default : printf("Humm.... ese no me lo se.\n"); break;
}
}
else printf ("Solo me trato con letras minusculas. \n") ; printf("Introduzca otra letra o un #.\n"); } /* fin del if de nueva linea */ } / * fin del while * /
La sentencia switch funciona de la siguiente forma: a continuación de la palabra switch hay una expresión entre paréntesis; dicha expresión se evalúa, y, en nuestro caso, el valor que posea se asigna finalmente a ch. A continua ción, el programa rastrea la lista de “etiquetas” (case “a” :, case “b” :, etc., en nuestro ejemplo) hasta que encuentra una que corresponda a dicho valor; entonces se transfiere el control del programa a dicha línea. ¿Y qué sucede si ninguna encaja? En ese caso se utiliza la línea marcada default:; el progra ma salta allí. En cualquier otra circunstancia, el programa continúa con la sentencia que sigue al bloque switch. ¿Cuál es la misión de la sentencia break? Esta sentencia hace que el pro grama se salga del switch y se dirija a la sentencia situada inmediatamente después del mismo (véase figura). Si no se hubiese colocado la sentencia break, se ejecutarían todas la sentencias situadas entre la etiqueta correspondiente y el final del switch. Por ejemplo, si eliminamos todas las sentencias break de nuestro programa y lo ejecutamos utilizando la letra d, obtendríamos la siguiente salida: Deme una letra y respondere con un nombre de animal que comience por ella. Pulse una letra; para terminar pulse #. d [return] destemplat, pinguino rojo de Kenia equigobo, canello siberiano Humm. ... ese no me lo se Introduzca otra letra o un #. # [return]
Como observará, todas las sentencias desde case “d” hasta el final del switch han sido ejecutadas. Figura 7.7
Programa de nombres de animales
195
Si conoce el lenguaje PASCAL, habrá asociado inmediatamente la sentencia switch a una similar que hay en este lenguaje, case. La diferencia mas importante entre ambas es que la sentencia switch requiere el uso de un break si se desea procesar únicamente la sentencia etiquetada. Las etiquetas de un switch deben ser constantes de tipo entero (incluyendo char) o bien expresiones de constantes (es decir, expresiones que conten gan únicamente constantes). En ningún caso se pueden emplear variables en las etiquetas. La expresión encerrada entre dos paréntesis debe tener valor entero (incluyendo también al tipo char). Así, la estructura general de un switch sería: switch(expresion entera)
{
case constante1 : sentencias; break; (opcional) case constante2 : sentencias; break; (opcional) default : sentencias; break; (opcional)
}
Se pueden utilizar etiquetas sin sentencias cuando deseamos que varias etiquetas den el mismo resultado. Así, el fragmento case ’F’ : case ’f’ :
printf("ferocissimus, lombriz de tierra mediterranea\n"); break;
haría que tanto F como f produjesen el mismo mensaje. Si se pulsa F, por ejemplo, el programa saltaría a dicha línea. Al no encontrar sentencias allí, el programa continuaría ejecutándose hasta alcanzar el break. En el programa hay también otros dos detalles que conviene mencionar. El primero viene determinado por la forma interactiva que hemos decidido darle. Nos referimos al empleo de # en lugar de EOF como señal de stop. Cualquier novicio en ordenadores se sentiría abrumado si se le pide introdu cir un carácter EOF o incluso un carácter de control, pero el símbolo # está bastante claro (incluso para ellos). Al no ser necesario que el programa lea un EOF, no hay tampoco necesidad de declarar ch de tipo int. Por otra par te, hemos colocado una sentencia if que hace que el programa ignore carac teres nueva línea. También esta segunda característica es una concesión a la “interactividad” del programa. Si no se hubiese introducido esta sentencia if, cada vez que pulsamos la tecla [return] se procesaría como carácter. ¿Cuándo debemos emplear un switch y cuándo una construcción else-if? Con frecuencia no tenemos elección. No se puede emplear switch cuando la elección esté basada en una comparación de variables o expresiones de tipo float; tampoco conviene usar un switch si la variable puede estar comprendi da en un cierto rango. Es muy simple escribir if (integer < 1000 && integer > 2)
pero intentar cubrir esta posibilidad con un switch implicaría teclearse eti quetas casi para todos los enteros comprendidos entre 3 y 999. Sin embargo, en general el switch es más eficiente en la ejecución del programa. RESUMEN: ELECCION MULTIPLE CON switch
I. Palabra clave: switch.
Suponemos en ambos casos qué número tiene el valor 2
II. Comentarios generales: El control del programa se transfiere a la sentencia cuya etiqueta tenga el mis mo valor que la expresión evaluada. El programa continúa ejecutando senten cia a sentencia hasta que se redirige de nuevo con un break. Tanto la expresión como las etiquetas deben tener valor entero (incluyendo el tipo char), y las eti quetas deben ser constantes o expresiones formadas únicamente por constan tes. Si no se encuentra ninguna etiqueta con el valor de la expresión, el control se transfiere a la sentencia etiquetada default, si existe. En caso contrario, el control pasa a la sentencia inmediatamente después de la sentencia switch.
F igu ra 7.8
Flujo de programa en switches, con y sin break.
197
Cuestiones y respuestas
III. Forma:
switch ( expresion ) {
case etiq1 : sentencia1 case etiq2 : sentencia2 default : sentencia3 }
Puede haber más de dos sentencias con etiquetas, y el caso default es op cional. IV.
Ejemplo:
switch ( letra ) case ’a’ : case ’i’ : printf ("%d es una vocal\n") ; case ’c’ : case ’s’ : printf("%d esta en la palabra \"casi\"\n", letra); default : printf("Que usted lo pase bien.\n");
}
Si letra tiene el valor “a” o “i”, se imprimen los tres lenguajes; si es “c” o “n”, se imprimen los dos últimos. Cualquier otro valor imprime únicamen te el último mensaje.
El material de este capítulo permite la preparación de programas mucho más poderosos y ambiciosos que antes. Simplemente compare los ejemplos de este capítulo con alguno de los dados anteriormente, y comprobará la exac titud de nuestra afirmación. Pero queda todavía un poco por aprender, y pa ra ello hemos preparado algunas páginas más para su lectura y entretenimiento.
Hasta ahora ahora hemos aprendido Cómo escoger entre ejecutar una sentencia o no: if Cómo escoger entre dos alternativas: if-else Cómo escoger entre alternativas múltiples: else-if, switch Los operadores de relación: > > = = = < = < ! = Los operadores lógicos: && || ! El operador condicional: ?:
Cuestiones 1. Indique de entre las siguientes proposiciones cuáles son ciertas y cuáles son falsas: a. 100 > 3 b. “a” > “c” c . 100 > 3 && “a” > “c” d. 100 > 3 || “a” > “c” e. ! (100 > 3) 2. Constrúyase una expresión para indicar las siguientes condiciones: a. número es igual o mayor que 1 pero menor que 9. b. c h no es una q ni una k . c. número está entre 1 y 9, pero no es 5. d. número no está comprendido entre 1 y 9. 3. El siguiente programa tiene expresiones de relación innecesariamente complejas, así como algunos errores. Simplifíquelo y corríjalo. main()
/* 1 */
{
/*
int peso, altura; / * en kilogramos y centímetros scanf("%d", peso, altura); i f (peso < 40) if (altura >= 172) printf("Es ud. muy alto para else if (altura < 172 && > printf("Es ud. alto para su else if (peso > 100 && ¡(peso <= if ( ¡(altura >= 148)) printf("Es bastante bajopara printf("Su peso es ideal.\n");
2 */
3 */ /* 4 */ /* 5 */
su peso. \n”); 164) peso.\n"); 100)) su peso.\n");
/* 6 */ /* 7 */ /* 8 */ /* 9 * / / * 10 */ / * 11 */ /* 12 */ /* 13 */ /* 14 */ /* 15 */
} / * 16 * /
Respuestas 1. Son ciertas a y d. 2. a. numero > =1 && numero < 9 b. ch != “q” && ch != “k” Nota: ch != “q” || ch != “k”sería siempre cierta, ya que si ch fuera una q no sería una k, siendo, por tanto, cierta la segunda alternativa, lo que implicaría que la combi nación completa sería cierta a su vez. c. número > 1 && número < 9 && número != 5 d. ! (número > 1 && número < 9) o bien número < = 1 || número > = 9 Nota: si decimos que un número no está comprendido entre 1 y 9, es lo mismo que decir que es menor o igual que 1 ó mayor o igual que 9. La segunda forma es más difícil de comprender a primera vista, pero como expresión es ligeramente más sencilla. 3. La línea 5 debe ser scanf (“%d %d”, &peso, &altura); no olvide los & en scanf( ). Asi mismo, esta línea debería estar precedida por una sentencia que solicitase los valores. Línea 9: lo que se indica en esta linea es (altura < 172 && altura > 164). Sin embargo, 199
la primera parte de la expresión no es necesaria, ya que altura debe ser menor de 172 por el else-if colocado en primer lugar. Así pues, una simple sentencia (altura > 164) hubiera bastado. Línea 11: la condición es redundante: la segunda subexpresión (peso no es menor ni igual a 100) significa lo mismo que la primera. Todo lo que se necesita es un simple (peso > 100), pero el mayor problema no está ahí; la línea 11 está unida al if incorrecto. Por la estructura del programa se ve claramente que se pretendía que este else estuviese unido a la línea 6. Sin embargo, está asociado al if de la línea 9, más reciente, según la regla antes comentada. Por tanto, la línea 11 se alcanzará cuando peso sea menor de 40 y altura sea 164 o menos. Ello hace imposible que peso sea mayor que 100 cuando se llegue a esta sen tencia. Las líneas 7 a 9 debieran estar encerradas entre llaves. Así, la línea 11 sería una alternativa de la línea 6, no de la 9. Línea 12: simplifique la expresión a (altura < 148) Línea 14: este else está asociado con el último if, colocado en la línea 12. Si se encierran las líneas 12 y 13 con llaves, se forzaría a que el else quedase asociado con el if de la línea 11. Observe que el mensaje final se imprime solamente en aquellos pesos comprendidos entre 40 y 100 kilogramos.
8 Bucles y tirabuzones En este capítulo encontrará: • El bucle while • Terminación de un bucle while • Algoritmos y seudocódigo • El bucle for • For da flexibilidad • El operador coma • Zenon encuentra el bucle for • Un bucle con condición de salida: do while • ¿Con qué bucle nos quedamos? • Bucles anidados • Otras sentencias de control: break, continue, goto • Evite el goto • Arrays • Una cuestión sobre entradas • Resumen • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios
203
Bucles y tirabuzones CONCEPTOS Bucles Bucles anidados Saltos en el programa Empleo de bucles con arrays
El bucle while Ya hemos utilizado ampliamente este bucle; nos limitaremos ahora a re pasarlo con un sencillo programa (quizá demasiado simple) que adivina nú meros. /* adivinanumeros */
/* un programa para acertar numeros bastante ineficiente */ #include main() { int sup = 1 ; char respuesta;
PALABRAS CLAVE while, do, for, break, continue, goto
OPERADORES
}
printf("Escoja un numero del 1 al 100. Tratare de "); printf("acertarlo. \nResponda s si es correcto y n"); printf("\nsi me equivoco.\n"); printf ("Hmm... su numero es el %d?\n", sup); while((respuesta = getchar()) != 's') /* toma respuesta */ if (respuesta != '\n') /* ignora caracter nueva linea */ printf("Entonces debe ser el %d ; correcto ?\n", ++sup) ; printf("Sabia que lo conseguiria!!!\n");
+ = -= * = /= % = ,
A medida que nos vamos exigiendo tareas más complejas, el flujo de los programas se vuelve más enmarañado. Necesitamos estructuras y sentencias para controlar y organizar el funcionamiento de los programas. El lenguaje C nos facilita una serie de comandos para ayudarnos a desarrollar estos ex tremos. Hasta ahora ya hemos visto lo útil que resulta el bucle while cuando se necesita repetir una acción varias veces. En C existen otras dos estructuras en bucle adicionales: el bucle for y el bucle do... while. En este capítulo com probaremos el funcionamiento de estas dos estructuras de control, e intenta remos sacarles el máximo partido; para ello discutiremos también el empleo de los operadores break, continue, goto y coma, los cuales pueden ser em pleados, asimismo, para controlar el flujo del programa. También tratare mos de pasada los arrays que a menudo se emplean en asociación con los bucles.
Observe la lógica del programa. Si se responde s, el programa abandona el bucle y se dirige a la sentencia printf final. El programa le pide que respon da con una n cuando su suposición no es correcta; pero de hecho, cualquier respuesta que no sea s enviará el programa a realizar una nueva iteración dentro del bucle. Sin embargo, si el carácter es nueva línea, queda ignorado. Cual quier otro carácter produce que se imprima como suposición el siguiente en tero. (¿Qué hubiese sucedido si se empleara sub+ + en lugar de + +sup?) La parte de sentencia if(respuesta != " \ n") indica al programa que ig nore los caracteres nueva línea que se transmiten al usar la tecla [enter]. El bucle while no necesita llaves, ya que la sentencia if, expresada en dos líneas, cuenta como una única sentencia. Habrá observado probablemente que éste es un programa bastante estú pido. Está escrito correctamente en C, y consigue la tarea que se le ha enco mendado, pero su forma de hacerla es muy ineficiente. Este ejemplo puede servir para indicar que la corrección no es el único criterio por el cual se ha de juzgar un programa; además, es importante la eficiencia. Volveremos más adelante con este programa e intentaremos ha cerlo un poco mejor. La forma general del bucle while es while (expresión) sentencia
Nuestros ejemplos han utilizado expresiones de relación en la parte llama da expresión, pero ésta puede ser una expresión cualquiera. La parte de sen205
tencia puede estar constituida por una única sentencia que acabe en un punto y coma de terminación o bien una sentencia compuesta encerrada entre lla ves. Si la expresión es cierta (en general, si su valor es distinto de 0), la sen tencia se ejecuta una vez y la expresión se evalúa de nuevo para comprobar si su certeza permanece intacta. Este ciclo de test y ejecución se repite hasta que la expresión se vuelve falsa (en general, 0). Cada ciclo realizado se deno mina una “iteración”. La estructura es muy similar a la de una sentencia if: la diferencia principal es que en la sentencia if el test y, posiblemente, la eje cución se realizan una sola vez, mientras que en el bucle while se puede repe tir un gran número de veces.
Este fragmento no es ninguna mejora. En él sí se cambia el valor de índi ce, ¡pero en dirección contraria! Por lo menos esta versión acabará termi nando cuando el índice alcance un valor inferior al más negativo permitido por el sistema. El bucle while está dentro de la categoría de bucles “condicionales”, em pleando una “condición de entrada”. Se llama condicional, ya que la ejecu ción de las sentencias depende de la condición que se describe en la parte de expresión: ¿Indice es menor que 5?; ¿el último carácter leído es un EOF? La expresión forma una condición de entrada, porque la condición debe cum plirse antes de acceder al cuerpo del bucle. En la siguiente situación el bucle no se ejecutará ni una sola vez, ya que la condición es falsa de principio. indice = 10; while ( indice++ < 5) printf("Que ud. lo pase bien o mejor aun\n");
Si cambia la primera línea a indice = 3;
el programa funcionará.
Algoritmos y seudocódigo Figura 8.1
Estructura de un bucle while
Terminación de un bucle while
Hay un punto CRUCIAL que debemos tener en cuenta cada vez que tra bajamos con bucles while. Cuando se construye un bucle de este tipo, se de be incluir algo que varíe el valor de la expresión de test, de manera que dicha expresión acabe por ser falsa. En caso contrario, el bucle no finalizará nun ca. Considere el siguiente ejemplo: indice = 1; while (indice < 5) printf("Buena suerte!\n");
Este fragmento imprime su cariñoso mensaje indefinidamente, ya que no hay nada que altere el valor inicial de índice, que estaba establecido en 1. indice = 1; while ( --indice < 5) printf("La primavera ataca de nuevo!\n");
Bien, es el momento de volver a nuestro casi inútil programa de acertar números. El defecto de este programa no está en la codificación per se, sino en el “algoritmo” empleado, es decir, el método utilizado para aceptar el número. Podemos representar este método de la forma siguiente: solicitar al usuario que piense un número el ordenador supone que es el 1 while suposición incorrecta, aumenta suposición en 1 Por cierto, esto es un ejemplo de “seudocódigo”, que es el arte de expre sar un programa en lenguaje normal imitando, por otra parte, el lenguaje del ordenador. El seudocódigo es una forma útil de trabajar con la lógica de un programa; una vez que la lógica parece correcta, se puede uno dedicar a los detalles de traducción del seudocódigo a un código de programación real. La ventaja del seudocódigo es que permite concentrarse en la lógica y organización del programa sin desperdiciar esfuerzos simultáneamente preo cupándose en cómo expresar las ideas en lenguaje de ordenador. En nuestro caso, si queremos mejorar el programa debemos mejorar el algoritmo. Un método sería escoger un número a mitad de camino entre 1 y 100 (50 es lo bastante próximo) y hacer que la parte humana del juego con teste si la suposición ha sido alta, baja o correcta. Si el usuario responde que 207
el número impreso es demasiado alto, quedarían automáticamente elimina dos todos los números entre 50 y 100. La siguiente suposición del programa debería ser un número entre 1 y 49, procurando que estuviese en la parte cen tral de este rango. Así, una nueva respuesta alto o bajo eliminaría la mitad de los números restantes y, continuando el proceso, el programa estrecharía rápidamente los límites hasta llegar al número correcto. Intentemos escribir estas ideas en seudocódigo. Llamaremos max y min, respectivamente, al má ximo y mínimo valor alcanzable por el número. En principio habíamos esta blecido estos límites en 100 y 1, de modo que comenzaremos con ellos. hacer max igual a 100 hacer max igual a 1 solicitar al usuario que piense un número supongo (max + min)/2 while sup incorrecta hacer: { si sup es alto, hacer max igual a sup menos 1 si sup es bajo, hacer min igual a sup más 1 nuevo sup es (max + min)/2} Observe la lógica del programa: si la primera suposición de 50 es alta, el máximo valor posible del número sería 49; por el contrario, si 50 es dema siado bajo, el mínimo valor sería 51. Realicemos ahora la traducción de este seudocódigo a C. En la figura 8.2 se presenta el programa. adivinanumeros2 * / una versión mejorada del anterior */ #include #define ALTO 100 #define BAJO 1 main() { int sup = (ALTO + BAJ0) /2; int max = ALTO; int min = BAJO; char respuesta; /* /*
printf ("Escoja un entero entre %d y %d. Tratare ", ALTO, BAJO); printf("de adivinarlo.\nResponda s si he acertado, a si mi ") ; printf("numero es demasiado alto\ny b si demasiado ">; printf("bajo.\n"); printf ("Hmm... su numero es el %d?\n", sup); while((respuesta = getchar ()) != 's') {
if (respuesta != '\n') { if (respuesta == 'a') { /* reduce limite superior si errado por exceso */ max = sup - 1 ; sup = (max + min)/2; printf("Demasiado alto... Entonces sera %d\n", sup);
}
else if (respuesta == 'b')
{ /* aumenta limite inferior si errado por defecto * / min = sup + 1; sup = (max + min)/2; printf ("Demasiado bajo... Entonces sera %d\n", sup);
}
else { / * indica respuestas correctas */ printf("No comprendo; utilice una s, una a "); printf("o una b.\n"); }
}
}
}
printf("Sabia que lo conseguiria!!\n"); Figura 8.2
Programa para acertar números
El else final permite al usuario una nueva respuesta cuando la anterior no se ajusta a una de las tres solicitadas. Observe también que hemos em pleado constantes simbólicas para hacer más sencillo el cambio de rango. ¿Fun ciona este programa? Veamos un ejemplo en el que hemos pensado el núme ro 71. Escoja un entero Responda s si he y b si demasiado Hmm... su numero
entre 1 y 100. Tratare de adivinarlo. acertado, a si mi numero es demasiado alto bajo. es el 50?
n
No comprendo; utilice una s, una a o una b. b Demasiado bajo... Entonces sera 75 a Demasiado alto... Entonces sera 62 b Demasiado bajo... Entonces sera 68 b Demasiado bajo... Entonces sera 71 s Sabia que lo conseguiría!!
¿Puede ir algo mal en este programa? Está protegido contra usuarios que tecleen respuestas incorrectas, de manera que esto no debe causar problemas. La única fuente de error posible es que alguien teclee a cuando debiera haber tecleado b, o viceversa. Por desgracia, no hay forma humana de hacer que nuestro eventual usuario sea veraz o que no se equivoque. Sin embargo, hay algunas reformas que podrían ser suficientemente interesantes (por ejemplo, para distraer a su sobrinito de seis años). Observe, en primer lugar, que esta nueva aproximación al problema necesita siete números como máximo para acertar cualquiera de ellos (cada suposición reduce las posibilidades a la mi tad; siete suposiciones cubrirían 27 — 1, ó 127, posibilidades, suficientes para manejar el centenar de números de partida). Podemos, por tanto, modificar 209
el programa para que cuente el número de suposiciones realizadas. Si este número supera a 7, se puede enviar un mensaje de protesta, y a continuación restituir a max, min y al contador sus valores originales. Otros cambios fac tibles para mejorar el programa podrían ser modificaciones de las sentencias if, de manera que aceptasen letras mayúsculas y minúsculas.
RESUMEN: LA SENTENCIA while I. Palabra clave: while II.
Comentarios generales:
La sentencia while crea un bucle que se repite hasta que la expresión de test se vuelve falsa, o 0. La sentencia while es un bucle con condición de entrada; la decisión de realizar una pasada más del bucle se realiza antes de que éste comience. Por tanto, es posible que el bucle se efectúe cero veces. La parte de sentencia dentro del bucle puede ser simple o compuesta.
El bucle for
I I I . Formato: while ( expresión ) sentencia
La porción sentencia se repite hasta que la expresión se vuelve falsa o 0. IV.
Aunque el formato utilizado es correcto, no es la mejor forma de abor dar este tipo de situaciones, ya que las acciones que definen al bucle no están agrupadas en un solo lugar. Ampliemos este punto. Un bucle que se ha de repetir un número fijo de veces lleva implícitas tres acciones. Se debe inicializar un contador, compararlo con un límite e incre mentarlo cada vez que se atraviesa el bucle. La condición de bucle while se preocupa de la comparación; por su parte, el operador incremento se encar ga de cambiar el valor del límite; tal como vimos anteriormente, se pueden combinar estas dos acciones en una sola expresión, empleando + + < = NUMERÓ. Por el contrario, la inicialización del contador se debe reali zar fuera del bucle, como hemos hecho en la sentencia cont = 1;. Como ve mos, esta tercera acción hace correr el peligro de que alguna vez nos olvide mos de inicializar el contador. Como es sabido, en programación las cosas malas que pueden suceder acaban sucediendo. Estudiaremos ahora una sen tencia de control que evita estos problemas.
Ejemplos:
Este bucle consigue agrupar las tres acciones en un solo lugar. Si emplea mos un bucle for, podríamos sustituir el fragmento anterior por una sola sen tencia: for (cont = 1; cont <= NUMERO; cont++) printf("Buena suerte!\n");
while (n++ < 100) printf("%d %d", n, 2*n + 1); while (chatos < 1000)
{
Esta expresión se ejecuta al final de cada bucle
chatos = chatos + ronda; ronda = 2 * ronda;
}
En el último ejemplo de bucle while se emplea una condición indefinida; no sabemos de antemano cuántas veces se va a ejecutar el bucle antes de que la expresión se vuelva falsa. En muchos de nuestros ejemplos, por el contra rio, hemos empleado bucles while con condiciones definidas, es decir, sabiendo el número de repeticiones de antemano. Un ejemplo de este segundo caso po dría ser: cont = 1; while (cont <=NUMERO) printf("Buena suerte!/n"); cont++;
/* inicializacion */ /* test */ /* accion */ /* incrementocont */
La expresión se inicializa una vez antes de comenzar el bucle
Figura 8.3
Estructura de un bucle for 211
La primera expresión es una inicialización; se realiza una sola vez, al comen zar el bucle for. La segunda es una condición de test; se evalúa antes de cada ejecución potencial del bucle; cuando la expresión es falsa (o, en general, 0) el bucle finaliza. La tercera expresión se evalúa al final de cada bucle. La hemos empleado para incrementar el valor de cont, pero no tiene por qué estar restringida a tal uso. El bucle for se completa con una sentencia simple o compuesta. En la figura 8.3 se ejemplariza la estructura de este bucle. En el siguiente ejemplo empleamos un bucle for en un programa que im prime una tabla de cubos: /* for al cubo */ main()
ellos. Esta flexibilidad está sustentada en la forma en que las tres expresiones de la especificación for pueden utilizarse. Hasta ahora hemos empleado la primera expresión para inicializar un contador; la segunda, para expresar el límite del mismo, y la tercera, para incrementar el contador en 1. Cuando se emplea de esta forma, la sentencia for de C es prácticamente como las de más que hemos mencionado. Pero existen muchas otras posibilidades, y a continuación mostraremos 9 de ellas. 1. Emplear el operador decremento para contar en sentido descendente en lugar de ascendente. for (n = 10; n > 0; n--) printf("%d segundos ! \n", n) ; printf("Contacto!!!\ n " );
{
}
int num; printf(" n n al cubo\n"); for (num = 1; num <= 6; num++) printf("%5d %5d\n", num, num*num*num) ;
Este programa imprime los enteros del 1 al 6 y sus cubos: n n al cubo
1
2. Contar de dos en dos, de diez en diez, etc., si así se desea. for (n = 2; n < 60; n = n + 13) printf(" %d \n", n);
En este ejemplo, n se incrementaría en 13 cada ciclo, imprimiendo los dígitos 2, 15, 28, 41 y 54. Por cierto, el C ofrece una notación abreviada para incrementar a una variable una cantidad fija. En lugar de
1
2
8
3
27
4
64
5 6
125 216
n = n + 13
podemos emplear n += 13
La observación de la primera línea del bucle for nos informa inmediata mente de todos los parámetros necesarios para el bucle: el valor inicial de num, el valor final del mismo y el incremento que num sufre en cada ciclo. Otro uso común de un bucle for es hacer un contador de tiempo, con el fin de adaptar la velocidad del ordenador a niveles humanos.
El símbolo + = es el “operador de asignación aditivo”, que suma cualquier cosa que se encuentre a su derecha al nombre de la variable situada a la izquierda. Véase el cuadro resumen para más detalles. 3. Se pueden contar caracteres en lugar de números for (ch = 'a'; ch <= 'z'; ch++)
for (n = 1; n <= 10000; n++)
Este bucle hace que el ordenador cuente hasta 10000. El punto y coma solitario de la segunda línea nos dice que el bucle no realiza ninguna otra tarea. Podemos pensar en el punto y coma como una “sentencia nula”, es decir, una sentencia que no hace nada. For de flexibilidad
Aunque el bucle for se parece al bucle DO de FORTRAN, al FOR de PAS CAL y al FOR...NEXT de BASIC, es mucho más flexible que ninguno de
printf("El caracter ASCII de %c es %d.\n" ch, ch) ;
Esta sentencia imprimiría las letras de la a a la z junto con sus va lores en código ASCII. Este bucle funciona porque el lenguaje C al macena los caracteres como enteros, de modo que el fragmento, a efec tos del programa, está contando enteros en cualquier caso. 4. Se puede comprobar alguna otra condición en lugar del número de iteraciones. Nuestro programa de cubos anterior podría tener, en lu gar de la sentencia for (num = 1; num <= 6; num++) 213
la siguiente: for (num = 1; num*num*num <= 216; num++)
en la cual estamos limitando el tamaño del bucle por el valor alcanza do por los cubos, y no por el número de iteraciones. 5. Se puede incrementar una cantidad en proporción geométrica en lugar de aritmética; es decir, en vez de sumar una cantidad fija cada vez, podemos multiplicar por una cantidad fija.
Observe que en el test está involucrado y, no x. Cada una de las tres expresiones del bucle for pueden emplear diferentes variables. Por otra parte, aunque el ejemplo es válido, no es síntoma de un buen estilo. El programa sería más claro si no se mezclasen procesos de cambio de índice con un cálculo algebraico. 7. Se pueden dejar una o más expresiones en blanco (pero no se olvide del punto y coma). En este caso, asegúrese simplemente de incluir dentro del bucle alguna sentencia que antes o depués consiga que aquél finalice.
for (deuda = 100.0; deuda < 150.0; deuda = deuda*1.1) printf("Su deuda asciende a % . 2f.\n"deuda);
ans = 2;
Este fragmento multiplica deuda por 1.1 en cada ciclo, incremen tándose por tanto un 10 por 100. La salida será: Su Su Su Su Su
deuda deuda deuda deuda deuda
asciende asciende asciende asciende asciende
a a a a a
100.00. 110.00. 121.00. 133.10. 146.41.
for (n = 3; ans <=25; )
ans = ans*n;
Durante la ejecución de este bucle el valor de n será constante e igual a 3. La variable ans, por su parte, comenzará con un valor 2, se incrementará a 6, 18 y obtendrá un valor final de 54. (El valor 18 es menor que 25, de manera que el bucle for realizará una nueva itera ción, multiplicando 18 por 3 para obtener 54.) Por otra parte, la sentencia
Como ya habrá imaginado, también hay una notación abreviada para multiplicar deuda por 1.1. La expresión en este caso es: deuda *= 1.1
el cual ejecuta la referida multiplicación. Como es lógico, el operador *= es el “operador de asignación multiplicativo”, el cual multiplica la variable situada a su izquierda por cualquier otra cosa que se en cuentre a su derecha (véase el cuadro resumen para mayor informa ción). 6. Se puede utilizar cualquier expresión legal que se desee como tercera expresión. En todos los casos, la expresión se evaluará tras cada itera ción. for (x=1; y <= 75; y = 5*x++ + 10) printf("%10d %10d\n", x, y);
Este fragmento imprime valores de x y de la expresión algebraica 5*x + 10. La salida sería como sigue: 1 2 3 4 5
for ( ; ; )
printf("Quiero ser algo en la vida!\n");
55 60 65 70 75
es un bucle infinito, ya que un test vacío se considera cierto. 8. No es necesario que la primera expresión inicialice una variable. En su lugar, puede ser, por ejemplo, una sentencia printf( ) de algún ti po. Recuerde que esta primera expresión se evalúa o ejecuta una sola vez, antes de entrar en el bucle. for (printf("Empiece a meter numeros\n"); num == 6; ) scanf("%d", &num); printf("Ese es el que yo queria!!!\n");
Este fragmento imprimiría el primer mensaje una vez, y continua ría aceptando números hasta que se introdujera un 6. 9.
Se pueden alterar los parámetros de las expresiones del bucle dentro del mismo. Por ejemplo, supongamos un bucle cuyo fragmento inicial es:
for (n = 1; n < 10000; n += delta)
Tras algunas iteraciones, se puede tomar la decisión de que delta es demasiado pequeño o demasiado grande. En ese caso, una senten cia if en el interior del bucle puede cambiar el valor de delta. Por otra parte, si nuestro programa es interactivo, delta puede ser alterado por el usuario en mitad del funcionamiento del propio bucle. En resumen, este bucle tiene una gran libertad en la selección de 215
las expresiones que lo controlan, lo que le hace mucho más útil que un simple repetidor de iteraciones. El potencial del bucle for se ve aún más aumentado por una serie de operadores que discutiremos más adelante.
diretes * = 2 es equivalente a diretes = diretes * 2 tiempo / = 2.73 es equivalente a tiempo = tiempo / 2.73 reduce % = 3 es equivalente a reduce = reduce % 3 Por la parte derecha se pueden emplear números o expresiones más ela boradas:
x *= 3*y + 12 es equivalente a x = x * (3*y + 12) RESUMEN: LA SENTENCIA for I. Palabra clave: for
II. Comentarios generales: La sentencia for emplea tres expresiones de control, separadas por puntos y coma, para controlar un proceso de bucle. La primera expresión de inicialización se ejecuta una sola vez antes de entrar al bucle. Si la expresión de test es cierta (distinta de 0), se ejecuta una vez el bucle completo. A continuación se evalúa la tercera expresión (actualización) y se comprueba de nuevo el test. La sentencia for es un bucle con condición de entrada; la decisión de realizar una nueva pasada del bucle se toma antes de atravesarlo. Es, por tanto, posi ble que el bucle no se ejecute ni una sola vez. La parte de sentencia de este bucle puede estar formada por una sentencia simple o compuesta. III. Formato: for (incialización; test; actualización)
sentencia El bucle se repite hasta que test se vuelve falso o 0. IV. Ejemplo: for (n = 0; n < 10; n++ )
printf (" %d %d\n", n, 2*n+l );
Estos operadores de asignación tienen la misma prioridad que igual, es decir, menor preferencia que + o *. Este hecho queda reflejado en el últi mo ejemplo. No es necesario, en realidad, utilizar estas formas; sin embargo, son más compactas y producen un código máquina mas eficiente que la forma lar ga. En particular, resultan de utilidad cuando se intenta encajar algo en una especificación de bucle for.
El operador coma El operador coma extiende aún más la flexibilidad del bucle for, ya que permite incluir más de una inicialización o actualización dentro de las especi ficaciones del bucle. Por ejemplo, el siguiente programa imprime tarifas pos tales. (Suponemos que la tarifa es de 20 pesetas para los primeros 5 gramos y 12 más por cada 5 gramos adicionales.) /* tarifas postales */ #define UNO 20 #define OTRO 12 main()
{
int gramos, costo;
OTROS OPERADORES DE ASIGNACION: + = , - = ,
}
print(" gramos costo\n"); for(gramos=5, costo=UN0; gramos <=50; gramos+=5, costo+=OTRO) printf("%5d %7d\n", gramos, costo);
*=,/=, %= Hace algunos capítulos mencionamos que existen varios operadores de asignación en C. Por supuesto, el más básico es =, el cual asigna simple mente el valor de la expresión de su derecha a la variable de su izquierda. Los demás son operadores de actualización de variable. Todos ellos emplean un nombre de variable a su izquierda y una expresión a su derecha. La va riable queda asignada a un nuevo valor igual a su valor antiguo operado por el valor de la expresión de su derecha. La operación concreta a que se somete la variable depende del operador. Por ejemplo:
tanteo + = 20 dimes - =2
es equivalente a tanteo = tanteo + 20 es equivalente a dimes = dimes -2
Las cuatro primeras líneas de salida serían: gramos costo 5 10
15
20 32
44
Hemos usado el operador coma en la primera y tercera expresión. Su pre sencia en la primera expresión hace que tanto gramos como costo se inicialicen. En la segunda expresión se conseguirá que gramos se incremente en 5,
217
y costo en 12 (el valor de OTRO) en cada iteración. Todos los cálculos nece sarios han sido realizados dentro de las especificaciones del bucle for. El operador coma no está restringido a este bucle, pero es donde se utili za con mayor frecuencia. El operador tiene otra propiedad más: garantiza que las expresiones separadas por él se evalúan de izquierda a derecha. Por tanto, gramos se inicializará antes que costo. En nuestro ejemplo concreto, este punto no es importante, pero sí lo sería si la expresión costo contuviese la variable gramos.
También se usa la coma como separador. Las comas de la sentencia char ch, fecha;
/= divide la variable i por la cantidad d. % = proporciona el resto de la división entera de la variable i por la canti dad d. Ejemplo: conejos *= 1.6; es equivalente a conejos = conejos * 1.6; II. Miscelánea: el operador coma El operador coma enlaza dos expresiones haciéndolas una sola, y garantiza que la expresión situada a la izquierda se evalúa en primer lugar. Su empleo más común está basado en la inclusión de más información en la expresión de con trol de un bucle for. Ejemplo:
o de printf("% d % d\n", tururu, tarara);
for (ronda = 2, chatos = 0; chatos < 1000; ronda *= 2) chatos += ronda;
son separadores, no operadores coma. Zenón encuentra el bucle for
Figura 8.4
El operador coma en el bucle for
RESUMEN: NUESTROS NUEVOS OPERADORES
Veamos cómo se puede utilizar el operador coma para resolver una vieja paradoja. El filósofo griego Zenón argumentó en una ocasión que una fle cha jamás podría alcanzar su blanco. El razonamiento era el siguiente: pri mero, la flecha recorre la mitad de la distancia hasta la diana; a continuación deberá recorrer la mitad de la distancia restante; todavía le queda por reco rrer la mitad de lo que queda, y así hasta el infinito. Al componerse la tra yectoria de un número infinito de partes, la flecha gastaría un tiempo infini to en alcanzar su destino. Sin embargo, estamos convencidos de que Zenón no se ofrecería como blanco voluntario para demostrar su poderoso argu mento. Transformemos esta idea en números, y supongamos que la flecha tarda un segundo en recorrer la primera mitad de su vuelo; por tanto, tardaría 1/2 segundo en viajar la mitad del resto, 1/4 de segundo en la mitad de lo que quedaba, etc. Podemos representar el tiempo total como una serie infinita 1 + 1/2 + 1/4 + 1/8 + 1/16 +... Escribamos un corto programa para averiguar la suma de los primeros términos. /* Zenon */ #define LIMITE 15 main() {
int cont; float suma, x;
I. Operadores de asignación:
Cada uno de estos operadores actualiza la variable de su izquierda utilizando el valor de su derecha en la operación indicada. En los ejemplos siguientes abre viamos izquierda y derecha como i y d, respectivamente. += suma la cantidad d a la variable i. -= resta la cantidad d de la variable i. *= multiplica la variable i por la cantidad d.
for(suma=0.0, x=1.0, cont=1; cont <= LIMITE; cont++, x *= 2.0)
{
suma += 1.0/x; printf("suma = %f en la etapa %d.\n", suma, cont);
}
} 219
La suma de los quince primeros términos sería:
se basa en cuándo se lee el carácter nueva línea. El bucle while imprime todos los caracteres hasta el carácter nueva línea exclusive, mientras que el do while imprimiría todos incluyendo el carácter nueva línea. Unicamente después de haberlo impreso se comprobaría el test de bucle. En resumen, en un bucle do while, la acción va antes de la condición del test. La forma general de un bucle do while es: do sentencia while (expresión);
Podríamos continuar añadiendo más términos, pero ya se observa que el total tiende a estabilizarse. Evidentemente, los matemáticos han demos trado que el total se aproxima a 2 conforme el número de términos tiende a infinito, exactamente igual que sugiere nuestro programa. Lo cual no deja de ser un hecho afortunado, porque si Zenón estuviese en lo cierto, el movi miento sería imposible. (Pero si el movimiento hubiera sido imposible, tam poco habría existido Zenón.) ¿Qué se puede decir del programa en sí mismo? Demuestra que se puede emplear más de un operador coma en una expresión. En este ejemplo hemos inicializado suma, x y cont. Una vez establecidas las condiciones del bucle, el programa en sí es muy sencillo.
La sentencia puede ser simple o compuesta. Un bucle do while se ejecuta siempre una vez como mínimo, ya que el test se realiza tras la ejecución de la iteración. Como ya se ha dicho, tanto el bucle for como el while
Bucle con condición de salida: do while Tanto for como while son bucles con condición de entrada. La condición del test se comprueba antes de cada iteración del bucle. Existe también en C un bucle con «condición de salida», en el cual la condición se comprueba al final de cada iteración. Esta variedad, llamada bucle do while, tiene el as pecto siguiente: do { ch = getchar () ; putchar(ch); } while (ch != ' \n' ) ;
La diferencia con el bucle while ( (ch = getchar ()) != ' \n') putchar(ch);
Figura 8.5
Estructura de un bucle do while
pueden ejecutarse 0 veces, ya que el test está colocado antes de la ejecución. En general, debemos restringir el uso de bucles do while a los casos que re quieran al menos una iteración. Por ejemplo, podríamos haber empleado un bucle do while en nuestro programa adivinador de números. La estructura en seudocódigo de dicho programa habría sido: do { hacer suposición obtener respuesta s, a o d } while (respuesta no sea s) 221
Por el contrario, se debe evitar la estructura do while en seudocódigo del tipo siguiente: preguntar usuario si desea continuar do trozo maravilloso de programa while (respuesta sea sí) Aquí, y aunque el usuario responda no, se ejecutará un maravilloso trozo de programa, ya que el test llega demasiado tarde.
jor mirar antes de saltar (o buclear) en lugar de hacerlo después. Una segun da razón se fundamenta en la claridad del programa; éste resulta más legible si la condición de test se encuentra al comienzo del bucle. Por último, existen muchas aplicaciones en las que es importante que el bucle se evite por com pleto si el test no se cumple en un primer momento. Supongamos que se ha decidido que necesita un bucle con condición de en trada, ¿cuál debe ser, for o while? En parte se trata de un problema de gus tos, porque, en último término, lo que puede hacer uno también lo puede hacer el otro. Así, si deseamos hacer un for idéntico a un while se puede omi tir la primera y tercera expresión: for ( ;test; )
RESUMEN: LA SENTENCIA do while I. Palabras clave: do, while
II. Comentarios generales: La sentencia do while crea un bucle que se repite hasta que la expresión de test se vuelve falsa o 0. La sentencia do while es un bucle con condición de salida; la decisión de pasar una vez más por el bucle se realiza después de haberlo atra vesado; por tanto, este bucle se ejecuta una vez como mínimo. La parte de sentencia del bucle puede ser simple o compuesta. III. Formato: do
sentencia
que es lo mismo que while (test)
Por el contrario, para hacer un while idéntico al for se debe empezar por una inicialización, e incluir tendencias de actualización: inicializar; while (test) { cuerpo; actualizar;
}
while (expresión);
La porción sentencia se repite hasta que expresión se hace falsa o 0. IV. Ejemplo: do scanf("%d", &numero)
que equivale a for (inicializar; test; actualizar) cuerpo;
while (numero != 20);
¿Con qué bucle nos quedamos? Una vez tomada en un programa la trascendental decisión de que necesi tamos un bucle, la pregunta que surge es: ¿cuál se utiliza? En primer lugar, hay que decidir si necesitamos un bucle con condición de entrada o de salida. En general la respuesta será que el bucle debe llevar condición de entrada. Kernighan y Ritchie estiman que un bucle con condición de salida (do while) se necesita en el 5 por 100 de los casos. Existen varias razones por las que los estudiosos de ciencias del cómputo consideran que un bucle con con dición de entrada es superior. Una de ellas es la regla universal de que es me
Por lo que concierne al estilo, parece apropiado usar un bucle for cuando el bucle implique inicializar y actualizar una variable; un bucle while, sin em bargo, puede resultar más apropiado si las condiciones son otras. Así, resul ta natural el empleo de while en while ((ch = getehar ()) != EOF)
El bucle for, por su parte, sería una elección más evidente en bucles que contengan contadores con un índice: for (cont = 1; cont <= 100; cont++)
223
Bucles anidados Se llama bucle anidado a aquel que está encerrado dentro de otro bucle. El problema que presentamos a continuación utiliza bucles anidados para en contrar todos los números primos hasta un determinado límite. Un número primo es aquel que puede dividirse únicamente por sí mismo y por la unidad. Los primeros números primos son 2, 3, 5, 7 y 11. Una manera muy directa de decidir si un número es primo es dividirlo por todos los números comprendidos entre 1 y él mismo. Si se encuentra que el número en cuestión es divisible por alguno de ellos, se deduce que ese nú mero no es primo. Podemos utilizar el operador módulo (%) para compro bar si la división es exacta. (Recordará el operador módulo, ¿no? Este ope rador da como resultado el resto de la división del primer operando por el segundo. Si un número es divisible por otro, el operador módulo dará como resultado 0.) Una vez encontrado un divisor, no es necesario seguir más ade lante; por ello debemos prever una terminación del proceso tan pronto como encontremos el primer divisor. Comenzaremos por comprobar un solo número. Para ello necesitamos un único bucle.
Si deseamos encontrar todos los números primos hasta un valor determi nado deberemos encerrar nuestro bucle for en otro bucle. Expresado en seudocódigo, for número = 1 a límite comprobar si número es primo La segunda línea representa el programa anterior. Traduciendo estas ideas a C obtenemos: /* primos2 */ main() { int numero, divisor, limite; int cont = 0; printf("Introduzca limite superior de busqueda de primos.\n"); printf("El limite ha de ser mayor o igual a 2.\n”); scanf("%d", &limite); while (limite < 2) /* otra oportunidad para errores */ { printf("No esta prestando atencion! Repita de nuevo.\n"); scanf("%d", &limite); } printf("Ahi van los primos.\n"); for (numero = 2; numero <= limite; numero++)/*bucle externo*/ { for (divisor = 2; numero % divisor != 0; divisor++) ; if (divisor == numero) { printf("%5d ", numero); if (++cont % 10 == 0) printf("\n"); }/* empieza nueva linea cada 10 primos */
/* primos1 */ main()
{
int numero, divisor; printf("Que numero quiere saber si es primo?\n"); /* toma respuesta */ /* no aceptar*/ {
scanf ("%d", &numero); while (numero < 2)
printf("Lo siento, no acepto numeros menores de 2. \n"); printf("Intentelo de nuevo\n"); scanf("%d", &numero); }
}
for (divisor = 2; numero % divisor != 0; divisor++) ; /* el test de primos se hace en las especificaciones */ if (divisor == numero) /* se ejecuta al terminar el bucle */ printf("%d es primo.\n", numero); else printf("%d no es primo.\n", numero);
} printf("\nEso es todo!\n"); }
El bucle externo selecciona cada número desde 2 hasta límite. En el bucle interno se realiza la comprobación. Utilizamos cont para calcular el número de primos. Cada diez números primos se comienza una nueva línea de impre sión. La salida podría ser:
Hemos incluido una estructura while para evitar valores de entrada que «estrellarían» el programa. Obsérvese que todos los cálculos se realizan en la sección de especifica ciones del bucle for. La cantidad número se va dividiendo por divisores cada vez mayores hasta que se encuentra un resultado exacto (es decir, número % divisor igual a 0). Si el primer divisor que cumple esta condición es el pro pio número, entonces, número es primo; en caso contrario habremos encon trado un divisor menor, y acabaremos el bucle antes. Eso es todo! 225
El algoritmo empleado es bastante directo, pero no es, por contra, una maravilla en eficiencia. Por ejemplo, si deseamos averiguar si 121 es primo, no hay necesidad de buscar divisores por encima de 11. Si hubiese algún divi sor mayor que 11, el resultado de la división sería un número menor que 11, el cual habría tenido que ser localizado anteriormente. En realidad, sólo hay que buscar divisores hasta la raíz cuadrada del número en cuestión; sin em bargo, la programación de este algoritmo es algo más complicada. La deja mos como ejercicio para el lector inteligente. (Clave: en lugar comparar el divisor con la raíz cuadrada del número, compárese el cuadrado del divisor con el propio número.)
Otras sentencias de control: break, continue, goto Las sentencias de bucle que hemos discutido hasta ahora, en unión con las sentencias condicionales (if, if-else y switch), contituyen los mecanismos de control más importantes en C. Con ellos se puede crear la estructura glo bal de un programa. Las tres sentencias que presentamos ahora se usan me nos frecuentemente; un abuso de las mismas hace que los programas sean difíciles de seguir, más propensos a errores y más difíciles de modificar. break: De las tres sentencias de control comentadas en este apartado, break es, sin duda, la más importante; es una vieja conocida nuestra que ya apareció al describir el comando switch; de hecho, su mayor importancia radica en la combinación con dicho comando, donde a menudo es necesaria, pero tam bién puede emplearse en combinación con cualquiera de las tres estructuras de bucle. Cuando el programa llega a esta sentencia, el flujo del mismo se desvía, liberándose del switch, for, while o do while, en donde se encontra ba, y dirigéndose a ejecutar la siguiente sentencia de programa. Cuando se usa la sentencia break en una estructura anidada, esta «liberación» afecta únicamente al nivel de la estructura más interna que la contenga. En ocasiones se utiliza break para abandonar un bucle cuando hay dos razones para dejarlo. En el siguiente ejemplo presentamos un bucle de «eco» que se detiene cuando lee un carácter EOF o un carácter nueva línea: while ( (ch = getchar() ) != EOF)
{
if (ch == ’\n’) break; putchar (ch);
}
Podríamos hacer la lógica del programa más clara colocando ambos tests en el mismo lugar: while ((ch = getchar()) != EOF && ch != '\n' ) putchar(ch);
Cuando utilice break como parte de una sentencia if estudie el caso por si se pudiera reescribir la condición (tal como hicimos arriba), de manera que se evite la propia inclusión del break. continue: Esta sentencia es utilizable en los tres tipos de bucles, pero no en switch. Al igual que break, interrumpe el flujo del programa; sin embargo, en lugar de finalizar el bucle completo, continue hace que el resto de la iteración se evite, comenzando una nueva iteración. Si reemplazamos el break del ejem plo anterior por un continue: while ((ch = getchar()) != EOF)
{
if (ch == ' \n' ) cont inue; putchar(ch);
}
el resultado obtenido es diferente del que se obtenía con la versión break. En esta nueva versión, continue hace que se ignoren los caracteres nueva lí nea; sin embargo, el bucle se abandonará únicamente cuando se localice un carácter EOF. De todas formas, también este fragmento podría haberse ex presado de manera más económica: while ( (ch = getchar()) != EOF) if (ch != '\n') putchar(ch);
Frecuentemente, como sucede en este caso, invirtiendo el test de la sen tencia if se elimina la necesidad de un continue. Por otra parte, la sentencia continue puede ayudar a acortar algunos pro gramas, en especial si existen varias sentencias if-else anidadas. goto: La sentencia goto, maldición del BASIC y FORTRAN, existe también en C. Sin embargo, el C, a diferencia de estos dos lenguajes, puede prescindir de ella casi por completo. Kernighan y Ritchie dedican a la sentencia goto el siguiente piropo: “usar la es abusar de ella”; sugieren que “se use con cuentagotas o nada en absoluto”. En primer lugar, comentaremos cómo utilizarla; después, por qué no de be hacerlo. La sentencia goto tiene dos partes: el goto y un nombre de etiqueta. La etiqueta tiene las mismas reglas y convenciones que se usan para nombrar una variable. Un ejemplo podría ser goto parte2;
227
Para que esta sentencia funcione debe haber otra sentencia que contenga la etiqueta parte2. Esta etiqueta se coloca al comienzo de la sentencia, sepa rándola de la misma por medio de dos puntos. parte2: printf("Analisis del refinado:\n");
La estructura if-else, de la que dispone el C, permite expresar esta elec ción mucho más limpiamente: if (iberos cascos = else cascos = celtas = 2
> 14) 3; 2; * cascos;
Evite el goto
En principio, no es necesario en ningún caso emplear goto en un progra ma C. Si su formación en programación procede de FORTRAN o BASIC, dos lenguajes que prefieren su uso, habrá desarrollado probablemente hábi tos de programación que dependen de la utilización de goto. Para ayudarle a desintoxicarse de esta dependencia, expondremos a continuación algunas situaciones familiares en las que se emplearía goto junto con una versión al estilo C.
3. Preparación de un bucle indefinido: leer: scanf("%d", &puntos); if (puntos < 0) goto etapa2; montones de sentencias; goto leer; etapa2: mas cosas;
1. Manejar un if que requiere más de una sentencia:
Utilice en su lugar un bucle while: if (talla > 12) goto a ;
goto b; a: costo = costo * 1.05; mo d o = 2 ; b: factura = costo * modo;
scanf("%d , &puntos); while (puntos >= 0) { montones de sentencias; scanf("%d", &puntos);
}
mas cosas;
(En BASIC y FORTRAN estándar, la única sentencia regida por el if es la inmediatamente posterior. Hemos traducido esta situación a su equivalente en C.) En C estándar se usa una sentencia compuesta o bloque de mayor sen cillez y elegancia. if (talla > 12) { costo = costo * 1.05; modo = 2;
}
4. Saltar al final del bucle: use en su lugar continue. 5. Abandonar un bucle: utilice break. De hecho, break y continue son formas especializadas de goto. Las ventajas de su empleo estriban en que sus nombres indican al usuario lo que se supone que deben hacer, y, al no requerir etiquetas, no hay peligro de colocar una etiqueta en el sitio equivocado. 6. Andar saltando alegremente de un sitio a otro del programa: sencilla mente, ¡no lo haga!
factura = costo * modo;
2. Elección entre dos alternativas: if (iberos > 14) goto a ; cascos = 2; goto b; a: cascos = 3; b: celtas = 2 * cascos;
Hay un único uso de goto que está tolerado por algunos veteranos del C, concretamente cuando se pretende salir de un conjunto de bucles anida dos en caso de error. (Recuérdese que un solo break liberaría exclusivamente del bucle más interno.) while (func > 0)
{ for (i = 1; i <= 100; i++)
{ for (j
{
=
1; j <= 50; j++ )
229
dos arrobas de sentencias; if (problema enorme) goto socorro; sentencias;
}
mas sentencias;
}
IV. continue
El comando continue puede utilizarse con cualquiera de las tres formas de bu cle, pero no con switch. Al igual que break, hace que el control del programa evite el resto de sentencias del bucle; sin embargo, a diferencia de él, este co mando inicia una nueva iteración. En el caso de bucles for o while, se comien za el siguiente ciclo del bucle. En el bucle do while se comprueba la condición de salida y, si procede, se empieza un nuevo ciclo.
todavia mas sentencias;
} y otras cuantas sentencias; socorro: arreglar el lio o marcharse dignamente;
Como habrá observado en nuestros ejemplos, las formas alternativas son más claras que la forma goto. La diferencia se hace aún más aparente cuan do se mezclan varias de estas situaciones a la vez. ¿Qué gotos están ayudan do a un if, cuáles están simulando if-else, quiénes están controlando bucles, cuáles están simplemente allí porque el programador ha decidido programar ese trozo en una esquina? Una buena sopa de gotos es la mejor receta para crear un laberinto en lugar de un programa. Si la sentencia goto no le es fa miliar, manténgase así. Si, por el contrario, está acostumbrado a emplearla, intente entrenarse en no hacerlo. Por ironías del destino, el C, que no necesi ta goto, tiene un goto mejor que la mayoría de los lenguajes, porque permite • utilizar palabras descriptivas como etiquetas en lugar de números.
RESUMEN: SALTOS EN PROGRAMA I. Palabras clave: break, continue, goto II.
Ejemplo:
while ( (ch = getchar()) != EOF) { if (ch == ' ') continue; putchar(ch); numcar++;
}
Este fragmento produce un “eco” y cuenta caracteres distintos del espacio. V. goto
La sentencia goto hace que el control del programa salte a una nueva sentencia con la etiqueta adecuada. Para separar la etiqueta de la sentencia etiquetada se emplea el símbolo dos puntos. Los nombres de etiquetas siguen las mismas reglas que los nombres de variables. La sentencia etiquetada puede aparecer antes o después del goto. Formato:
qoto etiqueta:
Comentarios generales:
Estas tres instrucciones hacen que el programa salte de una determinada loca lización a otra. III.
break
El comando break se puede utilizar con cualquiera de las tres formas de bucle y con la sentencia switch. Produce un salto en el control del programa, de ma nera que se evita el resto del bucle o switch que lo contiene, y se reanuda la ejecución con la siguiente sentencia a continuación de dicho bucle o switch. Ejemplo:
etiqueta: sentencia
Ejemplo: tope : ch = getchar(); if (ch != 's') goto tope;
switch (numero) {
case 4: printf("Ha elegido bien.\n"); break; case 5: printf("Es una excelente eleccion.\n"); break; default: printf("Su eleccion es un asco.\n");
} 231
Los arrays pueden ser de cualquier tipo de datos: int patos[22]; /*un array para almacenar 22 enteros * / char alfabeto[26] /* un array para almacenar 26 caracteres */ long gordos[5OO] / * un array para almacenar 500 enteros long */
Hemos hablado anteriormente, por ejemplo, de las tiras de caracteres, que son un caso especial de arrays de tipo char. (En general, un array char, es aquel cuyos elementos tienen asignados valores de tipo char. Una tira de caracteres es un array de tipo char en el cual se ha utilizado el carácter nulo, ‘\0’, para marcar el final de la misma.) Array de caracteres, pero no tira
Array de caracteres y tira
Carácter nulo
Figura 8.6 Arrays y tiras de caracteres
Arrays Los arrays son protagonistas de muchos programas. Permiten almace nar grandes cantidades de informaciones relacionadas de una forma conve niente. Dedicaremos más adelante un capítulo completo a los arrays, pero, como suelen estar a menudo merodeando bucles, es conveniente que comen cemos a utilizarlos ahora. Un array es una serie de variables que comparten el mismo nombre bási co y se distinguen entre sí con una etiqueta numérica. Por ejemplo, la decla ración
Los números empleados para identificar los elementos del array se lla man “subíndices” o simplemente “índices”. Los índices deben ser enteros, y, como ya se ha comentado, la numeración comienza por 0. Los elementos del array se almacenan en memoria de forma continuada, tal como se mues tra en la figura 8.7. (Dos bytes por entero)
Int boo [4]
float deuda[20];
anuncia que deuda es un array con veinte miembros o “elementos”. El pri mer elemento del array se denomina deuda[0]; el segundo, deuda[l], etc., hasta deuda[19]. Obsérvese que la numeración de los elementos del array comien za en 0 y no en 1. Al haber sido declarado el array de tipo float, cada uno de sus elementos podrá albergar un número en punto flotante. Por ejemplo, podríamos tener deuda[5] = 32.54; deuda[6] = 1.2e+21;
Char foo [4]
(Un byte por carácter)
Figura 8.7
Arrays de tipo char e int en memoria 233
Existe una infinidad de aplicaciones para los arrays. La que mostramos aquí es relativamente sencilla. Supongamos que necesitamos un programa que lea 10 resultados que serán procesados más adelante. Si utilizamos un array, evitaremos inventar 10 nombres diferentes de variables, uno para cada resul tado. Además, podremos emplear un bucle for para la lectura.
un valor mayor que el que en ese momento lleva máximo, hacemos máximo igual al nuevo valor localizado. Unamos ahora todos los trozos. Expresando en seudocódigo, leer los resultados imprimir un eco de los mismos calcular e imprimir promedio calcular e imprimir valor máximo
/* leer puntuaciones */ m a in ( )
{
Mientras estamos en ello, generalizaremos un poco más.
int i, a [10]; for (i = O; i <= 9; i++) scanf("%d, &a[i]); /* lee las 10 calificaciones */ printf("Las puntuaciones leidas son las siguientes:\n"); for (i = O; i <= 9; i++) printf("%5d", a[i]); /* comprobacion de la entrada */ printf("\n");
/ * calificaciones */ #define NUM 10 main() { int i, suma, media, máximo, valor [NUM];
}
Es una buena costumbre hacer que el programa repita como un “eco” los valores que acaba de leer. Así se asegura que los datos que está procesan do el programa son precisamente los que uno piensa. Como observará, el sistema empleado aquí es mucho más conveniente que el uso de 10 sentencias scanf( ) separadas para lectura y 10 sentencias printf( ) para verificación. El bucle for suministra una manera muy sencilla y directa de utilizar los índices del array. ¿Qué tipo de operaciones podemos realizar con estos resultados? Pode mos hallar su media, su desviación estándar (sí; sabemos como hacerlo); tam bién podemos calcular cuál es el mayor o, bien, ordenarlo. Nos ocuparemos ahora de las dos partes más fáciles: encontrar el promedio y hallar el valor mayor. Para hallar la media deberemos añadir este trozo al programa: int suma, media; for (i =0, suma = 0; i <= 9; i++) /* dos inicializaciones */ suma += a[i]; /* suma los elementos del array */ media = suma/10; /* ultramoderna forma de promediar */ printf("El promedio de puntuacion es %d.\n", media);
Por su parte, para hallar el valor máximo añadiremos este otro trozo: int maximo; for (maximo = a[0], i = 1; i <= 9; i++) if (a[i] > maximo) maximo = a [i] ;
printf("La puntuacion maxima es %d.\n", maximo);
En esta parte hemos comenzado haciendo máximo igual a a[0]. Seguida mente comparamos máximo con cada elemento del array. Si encontramos
}
printf("Introduzca los 10 valores.\n"); for (i = 0; i < NUM; i++) scanf("%d", &valor[i]); / * lee las 10 calificaciones */ printf("Las puntuaciones leidas son:\n"); for (i =0; i < NUM; i++) printf("%5d", valor[i]); /* comprueba entrada * / printf("\n") ; for (i = 0, sum = 0; i < NUM; i++) suma += valor[i] ; /* suma elementos del array */ media = suma/NUM; metodo ultramoderno de antes * / printf("El promedio de las calificaciones es %d.\n", media); for (maximo = valor[O], i = 1; i < NUM; i++) if (valor[i] > maximo) /* comprueba cual es mayor */ maximo = valor[i]; printf("La calificacion maxima es %d.\n", maximo);
Hemos sustituido 10 por una constante simbólica, aprovechado el hecho de que i = (NUM — 1) es lo mismo que i < NUM. Comprobaremos que funciona, y después haremos algunos comentarios adicionales. Introduzca los 10 valores. 76 85 62 48 98 71 66 89 70 77 Las puntuaciones leidas son: 76 85 62 48 98 71 66 89 El promedio de las calificaciones es 74. La calificación maxima es 98.
70
77
Un punto a denotar es que estamos empleando cuatro bucles for distin tos. El lector se preguntará si realmente es necesario hacerlo así. ¿Podríamos haber combinado algunas operaciones en un solo bucle? La respuesta es que sí; podríamos haberlo hecho; además, habríamos obtenido así un programa más compacto; sin embargo, nos hallamos deslumbrados por el principio de la modularidad (impresionables que somos). La idea que subyace detrás de esa frase es que un programa debe poderse dividir en unidades separadas o 235
módulos que realicen tareas independientes unas de otras. (Nuestro seudocódigo reflejaba los cuatro módulos de este programa.) Así se consigue que el programa sea más fácil de leer. Lo que es más importante: el programa es mucho más fácil de actualizar y modificar si cuidamos en no mezclar sus di ferentes partes. Se trata simplemente de arrojar al cesto de los papeles el mó dulo inservible, reemplazarlo por uno nuevo y dejar el resto del programa sin tocar. El segundo comentario a destacar es que un programa que procese exac tamente 10 números es bastante poco satisfactorio. ¿Qué sucede si alguien falla y tenemos sólo 9 resultados? Es cierto que empleando una constante simbólica para el 10 hemos conseguido que el programa sea fácil de cambiar, pero aún habría que compilarlo de nuevo. ¿Existen otras alternativas? Justa mente de eso vamos a tratar.
Una cuestión sobre entradas Hay varias formas de leer una serie de, por ejemplo, números. Ya hemos comentado algunas aquí, procurando ir de la menos a la más conveniente. En general, la forma menos conveniente es la que acabamos de utilizar: escribir un programa que acepte un número fijo de valores como dato de en trada (sin embargo, sería la aproximación correcta si el número de datos de entrada nunca fuera a cambiar). Por el contrario, si el número de datos cam bia, deberemos recompilar el programa. La siguiente manera (mejor) de hacerlo es preguntar al usuario cuántos datos va a introducir. Como el tamaño del array está fijado por el programa, éste deberá comprobar previamente si la respuesta del usuario supera el pro pio tamaño del array. Una vez hecho este test, se pueden empezar a introdu cir datos. Esta idea correspondería a la remodelación del comienzo de nues tro programa anterior:
El problema que surge con este método es que deja en manos del usuario operaciones tan delicadas como contar correctamente el número de datos e introducir dicho número. Un programa que se base en que el usuario haga las cosas correctamente suele ser bastante frágil. Así llegamos a la siguiente aproximación al problema, en la cual el orde nador se encarga de contar el número de datos introducidos; después de to do, los ordenadores han demostrado desde siempre cierta aptitud en contar números. Ahora surge un pequeño problema subsidiario, que consiste en bus car la manera de indicar al ordenador cuándo se ha finalizado la entrada de números. Uno de los métodos es hacer que el usuario teclee un signo especial para anunciar el fin de datos. Esta señal debe ser del mismo tipo de datos que el resto, ya que ha de ser leída por la misma sentencia de programas. Pero, por su propia naturale za, debe ser distinta de los datos ordinarios. Por ejemplo, si estamos leyendo puntuaciones de un test en el cual la calificación alcanzada por una persona puede oscilar entre 0 y 100 puntos, no podríamos emplear 74 como señal, ya que podría ser una puntuación real. Sin embargo, un número como 999 ó —3 podría utilizarse para esta función, ya que no puede, en ningún caso, representar una calificación. Una implementación de este método sería: #define STOP 999 /* signo de detencion de la entrada */ #define NUM 50 ma i n ( ) { int i, cont, temp, valor[NUM] printf("Comience a meter datos. Teclee 999 para indicar \n"); printf("el final de los mismos. El numero maximo de datos \n"); printf( "aceptable es %d,\n", NUM); cont = 0; scanf("%d",&temp) ; /* lee un valor */ while (temp != STOP && cont <= NUM) /* ve si hay que parar */ { /* y tambien si queda sitio para el nuevo dato */ valor[cont++] = temp; /* guarda el dato y actualiza cont */ if (cont < NUM + 1) scanf("%d", &temp) ; /* lee el siguiente valor */ else printf("Ya la has hecho!!! No me caben mas!!!\n); } printf("Ha introducido %d datos, a saber:\n", cont); for (i = 0; i < cont; i++) printf("%5d\n", valor[i]);
printf("Cuantos datos va a introducir?\n"): scanf("%d", &ndat); while (ndat > NUM)
{
printf("Solo puedo manejar %d datos; introduzca "); printf("un numero menor.\n", NUM); scanf("%d", &dat);
} /* se asegura que ndat <= NUM, dimension del array */ for (i =0; i < ndat; i++) scanf("%d", &valor[i]);
A continuación deberíamos reemplazar todos los NUM del programa (ex cepto en la sentencia #define y en la declaración del array) por ndat. Así se consigue que el resto de operaciones afecten únicamente a aquellos elemen tos del array que tienen datos asignados.
}
Hemos leído los datos en una variable temporal temp y asignado su valor al array únicamente después de comprobar que no coincidía con la señal de parada. En realidad, no es necesario hacerlo de esta forma; simplemente pen samos que así se evidencia más el proceso de comprobación. Observe que hemos comprobado dos cosas: primero, si se había leído la señal de stop; segundo, si hay espacio suficiente en el array para otro núme ro. Si llenamos el array antes de recibir la señal de stop, el programa informa cortésmente que se acabó la entrada de datos, y abandona el proceso. 237
Hasta ahora hemos aprendido Observe, además, que estamos usando la forma sufija del operador in cremento. De este modo, cuando cont es 0, se asigna temp a valor[0], y se guidamente cont se incrementa en 1. Al terminar cada iteración del bucle while, cont aventaja al último índice utilizado en el array en 1. Esta situación es precisamente la que queremos, ya que al ser valor[0] el primer elemento del array, valor[20] será el elemento número 21, y así sucesivamente. Cuando el programa abandone finalmente el bucle, en cont se contendrá el número total de datos leídos. A continuación podemos emplear cont como límite su perior para los bucles for subsiguientes. Este esquema funciona bien en tanto en cuanto dispongamos de una re serva de números que no pueden ser utilizados como datos. ¿Qué sucedería si debemos preparar un programa que acepte cualquier número del tipo ade cuado como dato? Nos veríamos entonces en la tesitura de que no es posible emplear un número como señal de stop. Encontramos un problema parecido cuando estudiamos el carácter fin de fichero (EOF). La solución empleada entonces fue utilizar una función que capturase caracteres (getchar( )), que en realidad es de tipo int. Así se permi tía que la función leyese un “carácter” (EOF) que no era ciertamente un ca rácter ordinario. Lo que necesitamos ahora es una función que capture ente ros, pero que sea también capaz de leer un no entero que pueda emplearse como símbolo de parada. Llegados a este punto tenemos que darle algunas noticias. Las buenas son que es posible encontrar esa función. Las malas, que necesitamos aprender algo más acerca de las funciones, de manera que tenemos que posponer el desarrollo de esta idea hasta el capítulo 10.
Resumen El tema fundamental de este capítulo ha sido el control del programa. En C se ofrecen muchas ayudas para la estructuración de los programas: las sentencias while y for proporcionan bucles con condición de entrada. Esta última es particularmente adecuada en bucles que llevan aparejadas inicializaciones y actualizaciones. El operador coma permite inicializar y actualizar más de una variable dentro de las especificaciones de un bucle for. También tenemos, para las pocas ocasiones en que es necesario, un bucle con condi ción de salida: la sentencia do while. Hay otras sentencias, como break, con tinue y goto que facilitan nuevos medios de control del programa. Más adelante estudiaremos con detalle los arrays. Lo que hemos visto aquí se puede resumir como: los arrays se declaran de la misma forma que las va riables ordinarias, añadiendo un número entre corchetes para indicar el nú mero de elementos. El primer elemento de un array es el número 0, el segun do el número 1, etc. Los subíndices que se utilizan en la numeración de arrays se pueden manipular convenientemente con el empleo de bucles.
Las tres formas de bucles en C: while, for y do while La diferencia entre condición de entrada y condición de salida en bucles Por qué se emplean con mucha mayor frecuencia bucles con condición de entrada que con condición de salida Los demás operadores de asignación: + = -= *= /= % = Cómo usar el operador coma Cuándo se utilizan break y continue: rara vez Cuándo se utiliza goto: cuando lo que se desea es un programa complica do y difícil de seguir Cómo usar un while para proteger el programa de entradas erróneas Qué es un array y cómo se declara: long aniza[8]
Cuestiones y respuestas Cuestiones
1. Encuéntrese el valor de cuac tras la ejecución de cada línea. int cuac = 2; cuac += 5; cuac *= 10; cuac -= 6; cuac /= 8; cuac %= 3;
2. ¿Qué salida produciría el siguiente bucle? for (valor = 36; valor > 0; valor /= 2) printf("%3d", valor);
3. ¿Cómo modificaría las sentencias if de adivinanúmeros2 de manera que acepta sen letras mayúsculas y minúsculas? 4. Sospechamos que el siguiente programa no es perfecto. ¿Cuántos errores puede detectar? main() { int i, j, listado); for (i = 1, i <= 10, i++) { list[i]=2*i+3; for (j = 1, j >= i, j++) printf("%d\n", list[j]); }
/* linea 1*/ /* linea 2*/ /* linea 3* / /*
/* /* /* /* /*
linea linea linea linea linea linea
5*/ 6*/ 7*/ 8*/ 9*/ 10 * / 239
5.
Escriba un programa que reproduzca el siguiente diseño utilizando bucles anid dos. $$$$$$$$ $$$$$$$$ $$$$$$$$
6. Escriba un programa que cree un array con 26 elementos y almacene en él las 26 letras minúsculas. Respuestas 1.2, 7, 70, 64, 8, 2 2.36 18 9 4 2 1. Recuerde cómo funciona la división entera. 1 dividido por 2 es igual a 0 de manera que el bucle termina cuando el valor se iguala a 1 3. if (respuesta == ’a’ || respuesta == ’A’) 4. Línea 3: debe ser lista[10] Línea 5: sustituir las comas por puntos y comas Línea 5: el rango de i debe oscilar entre 0 y 9, no entre 1 y 10 Línea 8: sustituir las comas por puntos y comas Línea 8: se debe utilizar > = en lugar de < =. De otro modo, cuando i sea 1, el bucle es infinito Línea 10: se debe incluir otra llave de cierre entre las líneas 9 y 10. Una llave cierra la sen tencia compuesta y la otra cierra el programa. 5. main()
{
int i, j ; for (i = 1; i <= 4; i++)
{
for
}
}
printf("\n");
6.
main() { int j ; char ch, alfa[26];
y
for (i =0, ch = ’a’ ; i < 26.; i++, ch++) alfa[i] = ch;
Ejercicios 1. Modifiquese adivinanúmeros2 con las directrices que apuntábamos, para mejorar el programa. 2. Implemente la sugerencia hecha anteriormente, para mejorar la eficiencia de nú meros primos. 3. Utilizando bucles anidados, produzca el siguiente diseño: 241
9 Funcionamiento funcional de las funciones En este capítulo encontrará: • Creación y utilización de una función sencilla • Argumentos de funciones • Definición de una función con argumentos: argumentos formales • Llamada a una función con argumento: argumentos efectivos • La función como caja negra • Argumentos múltiples • Devolución de un valor desde una función: return • Variables locales • Localización de direcciones: el operador & • Alteración de variables en el programa de llamada • Punteros; un primer repaso • El operador de indirección: * • Declaración de punteros • Utilización de punteros para comunicaciones entre funciones • A ver cómo funcionamos • Cómo especificar tipos de funciones • Todas las funciones C se crean de la misma manera • Resumen • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios
243
Funcionamiento funcional de las funciones CONCEPTOS Funciones Ensamblaje de un programa Comunicación entre funciones: argumentos, punteros, return Tipos de funciones
PALABRAS CLAVE return
tinas y procedimientos de otros lenguajes, aunque los detalles pueden ser di ferentes. Algunas funciones producen acciones concretas; por ejemplo, printf( ) hace que los datos se impriman en pantalla. Otras buscan un deter minado valor para que pueda ser empleado por el programa; así, strlen( ) informa al programa sobre la longitud de una determinada tira de caracte res. En general, una función produce acciones, o suministra datos, o ambas cosas a la vez. ¿Por qué usamos funciones? La razón principal es para evitarnos tediosas repeticiones de programación. Escribiendo una sola vez la función apropia da, podremos emplearla cualquier número de veces en un determinado pro grama, en diferentes situaciones y localizaciones del mismo, lo que evita te ner que hacer lo propio en el programa original. También se puede emplear la misma función en diferentes programas, tal como hemos hecho, por ejem plo, con putchar( ). Aun cuando la función se ha de emplear en el programa una sola vez, resulta bastante útil su uso, ya que un programa distribuido en funciones es más modular; por tanto, más fácil de leer y de cambiar o arreglar. Supóngase, por ejemplo, que deseamos escribir un programa que realice las siguientes tareas: leer una lista de números ordenar los números calcular su media imprimir un diagrama de barras Podríamos utilizar un programa como éste: main() { float lista[50];
} La filosofía del diseño del C está basada en el empleo de funciones. Has ta aquí hemos usado algunas funciones que nos ayudaban en nuestra progra mación: printf( ), scanf( ), getchar( ), putchar( ) y strlenf( ). Estas funcio nes venían ya dispuestas en el sistema, pero también hemos creado algunas por nuestra cuenta, todas ellas llamadas main( ). Los programas comienzan siempre ejecutando las instrucciones de main( ); una vez iniciado el progra ma, main( ) puede llamar a otras funciones, como getchar( ). En este capí tulo aprenderemos a crear otras funciones, y a hacerlas comunicarse con main( ) y entre ellas mismas. Pero antes de todo, ¿qué es una función? Una función es una unidad de código de programa autocontenida, diseñada para realizar una tarea deter minada. Las funciones en C juegan el mismo papel que las funciones, subru
leerlista(lista); ordenar(lista) ; promedio(lista); diagrama(lista);
Por supuesto, también tendríamos que escribir nuestras cuatro funciones: y diagrama( ), pero esto son simples de talles. Al utilizar nombres descriptivos de las funciones, hemos dejado bas tante claro lo que el programa hace y cómo está organizado. A continuación podemos «afinar» cada función por separado hasta con seguir que haga lo que se pretenda de ella. Por otra parte, si las funciones son lo suficientemente generales, podremos obtener el beneficio extra de apli carlas a otros programas. Muchos programadores se plantean las funciones como «cajas negras», definidas exclusivamente por la información que hay que suministrarles (su entrada) y el producto recibido (su salida); lo que suceda dentro de la caja leerlista( ). ordenar( ). promedio( )
245
Los puntos más importante a destacar de este programa son los siguien tes: negra no es de nuestra incumbencia, a menos que seamos nosotros mismos los que hemos diseñado tal función. Por ejemplo, cuando usamos printf( ), sabemos que hay que entregarle una tira de control y, opcionalmente, algu nos argumentos. También sabemos qué salida produce printf( ) con esos da tos. Lo que no nos preocupa es la programación interna que contiene printf( ), y que la hace comportarse de tal modo. Si consideramos las funciones de es ta manera, nos podremos concentrar en el diseño global del programa, en lugar de preocuparnos por los detalles. ¿Qué necesitamos saber de las funciones? En primer lugar, aprender có mo se las define adecuadamente, cómo llamarlas para su utilización y cómo establecer comunicaciones entre una función y el programa que la llama. Co menzaremos tratando estos puntos por medio de un ejemplo muy sencillo, para irnos remontando después hasta dominar adecuadamente el tema.
1. Hemos llamado (invocado, solicitado) la función asteriscos( ) en el pro grama main( ) simplemente escribiendo su nombre. El sistema recuer da un poco al empleado para conjugar un demonio; pero en lugar de dibujar un pentágono, nos limitamos a colocar tras el nombre un pun to y coma, con el fin de crear una sentencia: asteriscos();
Es ésta una forma de llamar funciones, pero no es la única. Cuan do el ordenador encuentra la sentencia asteriscos( ), se dirige a la fun ción de ese nombre y sigue las instrucciones indicadas allí. Al termi nar, retorna a la siguiente línea del “programa de llamada”, en este caso main( ).
creación y utilización de una función sencilla Nuestro primer objetivo (modesto, ciertamente) consiste en crear una fun ción que imprima 65 asteriscos en fila. Con el fin de situarla en un contexto, se incluirá dentro de un programa para imprimir encabezados de cartas. Se guidamente presentamos el programa completo; está compuesto por las fun ciones main( ) y asteriscos( ). /* encabezado1 */ #define NOMBRE "ORDENATAS, S.A." #define DIREC "Plaza del Byte 12" #define CIUDAD "Villabits, E- 60006" main() { Asteriscos() ; printf ("%s\n", NOMBRE); printf("%s\n", DIREC); printf("%s\n", CIUDAD) ; asteriscos() ;
} /* Ahora viene la funcion asteriscos */ #include #define LIMITE 65 asteriscos()
{
int cont;
}
for (cont = 1; cont <= LIMITE; cont++) putchar('*'); putchar('\n') ;
Figura 9.1 Flujo de control de encabezado 1 247
2. Obsérvese que hemos empleado el mismo formato para escribir aste. riscos( ) que para main( ). En primer lugar viene el nombre; a conti nuación la llave de abrir, la declaración de variables empleadas, las sentencias de definición de la función y la llave de cierre. Incluso he mos precedido la función con sentencias del tipo #define y #include que se necesitaban en ella, pero no en main( ).
Instrucciones de preprocesador Nombre de la función
Sentencia de declaración Sentencia de control de bucle Sentencia de función Sentencia de función
Figura 9.2
Estructura de una función sencilla
3. Se ha incluido asteriscos( ) y main( ) en el mismo fichero. También po dríamos haber utilizado dos ficheros distintos. Las dos formas tienen sus ventajas e inconvenientes. Un solo fichero es ligeramente más fácil de compilar. Dos ficheros separados, por su parte, simplifican la utili zación de la misma función en programas diferentes. Discutiremos más adelante el empleo de dos o más ficheros. Por ahora pondremos todas nuestras funciones en la misma cesta. El compilador advierte que main( ) ha terminado, al encontrar su llave de cierre; los paréntesis de asteriscos( ) indican al compilador que lo que sigue es una función. Obsérvese, además, que asteriscos( ) no va seguido por un punto y coma; esta ausencia de punto y coma indica al compilador que estamos defi niendo asteriscos( ), no utilizándola. Podemos imaginar, como ya indicábamos, que asteriscos( ) es una caja negra cuya salida es la línea de asteriscos que se imprime. No hay en este ca so entrada alguna, ya que la función no necesita ninguna información de! programa de llamada; dicho de otra forma, no existe en nuestro ejemplo nin guna comunicación entre ambos programas. Vayamos a un nuevo caso en el que sí se necesita comunicación.
Argumentos de funciones El encabezado de la carta quedaría más agradable si consiguiésemos que el texto estuviera centrado. Se puede centrar el texto imprimiendo un núme ro adecuado de espacios en blanco antes del resto de la línea. Vamos a escribir una nueva función que imprima espacios. La función espacios( ) (la vamos a llamar así) se asemejará bastante a la función asteris cos( ), con la importante salvedad de que esta vez necesitamos comunica ción entre main( ) y la función, con el fin de que aquel indique a ésta cuán tos espacios debe imprimir. Seamos más específicos. Nuestra barra de asteriscos ocupa 65 posiciones, mientras que ORDENATAS, S.A. tiene 15 espacios. Así, en nuestra primera versión quedaban 50 espacios libres a continuación del renglón de encabeza miento. Si deseamos centrarlo, deberemos dejar 25 espacios a la izquierda, con el fin de que queden los 25 restantes a la derecha; por tanto, deseamos comunicar el valor «25» a la función espacios. Usaremos el mismo método que empleamos para comunicar el valor ‘ * ’ a putchar( ): utilizar un argu mento. Por tanto, espacios(25) significará que deseamos saltar 25 espacios; 25 es el argumento. Llamaremos tres veces a la función espacios( ), una por cada línea de la dirección. El aspecto del programa general sería: /* encabezado2 */ #define NOMBRE "ORDENATAS, S.A." #define DIREC "Plaza del Byte 12" #define CIUDAD "Villabits, E- 60006" main () {
int salta; asteriscos(); espacios(25); /* espacios con una constante como argumento */ printf("%s\n", NOMBRE); salta = (65 - strlen(DIREC))/2; /* dejamos al programa calcular los espacios a saltar */ espacios(salta); printf("%s\n", DIREC); espacios((65 - strlen(CIUDAD))/2);/* expresion como argumento*/ printf("%s\n", CIUDAD); asteriscos();
} /* Aqui esta otra vez asteriscos() */ #include #define LIMITE 65 asteriscos()
{ int cont;
}
for (cont = 1; cont <= LIMITE; cont++) putchar ('*'); putchar('\n');
249
/* Y aqui viene espacios() */ espacios(numero) int numero; /* el argumento se declara antes de la llave */
{
int cont;
lizará su tarea; hemos dado un valor a número utilizando un “argumento efectivo” o “parámetro” en la llamada a la función. Concentrémonos en la primera vez que usamos espacios( ):
/* declara otra variable dentro de las llaves */ espacios(25);
}
for (cont = 1; cont <= numero; cont++ ) putchar(' ');
El parámetro 25 es un valor que se asigna al argumento formal, la varia ble número; es decir, la llamada a función tiene el efecto siguiente:
Figura 9.3
Programa de encabezamientos
Ya que estábamos en ello, hemos aprovechado el programa para experi mentar tres formas diferentes de expresión del argumento. ¿Funcionan to das? Sí, y aquí está la prueba
numero = 25;
En resumen, el argumento formal es una variable en el programa que ha sido llamado, y el argumento efectivo es el valor particular asignado a tal variable en el programa de llamada. Tal como se muestra en nuestro ejem plo, este argumento específico o parámetro puede ser una constante variable o incluso una expresión más complicada. En cualquiera de los casos se eva lúa el argumento y se envía su valor a la función (en este caso, un entero). Si nos fijamos en la última llamada, espacios( ) e s p a c i o s ( (65
Primero trataremos de la preparación de una función con argumento; des pués estudiaremos cómo utilizarla. Definición de una función con argumento: argumentos formales
Nuestra definición de función comienza con dos líneas: espacios(numero) int numero;
La primera línea indica al compilador qué espacios( ) emplea un argu mento, y que tal argumento se llama número. La segunda línea es una decla ración que informa al compilador qué número es de tipo int. Nótese que el argumento se declara antes de la llave de comienzo del cuerpo de la función. Por cierto, se pueden condensar estas dos líneas en una:
- strlen (CIUDAD))/2);
nos percataremos que la expresión que forma el argumento se evaluó obte niendo un resultado de 26; a continuación se asignó el valor 26 a la variable número. La función, de hecho, ignora o no le preocupa si el número proce día de una constante variable o expresión. Insistimos, el parámetro enviado es un valor específico que se asigna a la variable conocida como argumento formal. Argumento "formal" = nombre creado por la definición de la función
espacios (int numero;)
De cualquiera de las dos formas la variable número se denomina un argu mento “formal”. A todos los efectos es una nueva variable, y el ordenador se encarga de colocarla en una dirección de memoria separada. Observemos ahora cómo se utiliza esta función.
Argumento "efectivo" = parámetro = valor = 25 inicializado en main( )
Llamada a una función con argumento: argumentos efectivos
El truco consiste en asignar un valor al argumento formal, en este caso número. Una vez conseguido que la variable tenga un valor, el programa rea
Figura 9.4
Argumentos formales y efectivos 251
/* test de abs */ main() {
La función como caja negra
Tomando nuestra función espacios( ) como una caja negra, considerare mos únicamente que la entrada es el número de espacios que se han de saltar, y la salida es el salto de dichos espacios. La entrada se comunica a la función por medio de un argumento: éste es, de hecho, el vínculo de comunicación entre main( ) y espacios( ). Por otra parte, la variable cont, declarada dentro de la función, es de uso interno de la misma, y las demás funciones ignoran su existencia. Este hecho forma parte de la perspectiva caja negra ya comen tada. Dicha variable cont no es la misma que la cont empleada en asteriscos( ). Argumentos múltiples
Cuando se necesita enviar más de un argumento, se puede formar una lista separando los distintos argumentos por comas:
int a = 10, b = 0, c = - 22; int d, e, f; d = abs( a); e = abs(b); f = abs( c); pri ntf ("%d %d %d", d, e, f) ;
} /* funci on valor absolut o */ abs( x) int x; { int y; y= ( x < o) ? -x : x; /* recuer de el oper ador ?: */ ret urn (y) ; /* devuel ve el val or de y a main */
}
Y la salida: printnum (i, j )
int i, j ;
}
printf("Nuevo valor = %d. Total acumulado = %d \n”, i, j) ;
Ya hemos visto cómo comunicar información desde el programa de lla madas a la función. ¿Qué hay del flujo de información en sentido opuesto? Ese es precisamente nuestro siguiente apartado.
construción de un valor desde una función: return Nuestro siguiente objetivo es la construcción de una función que calcule el valor absoluto de un número. Se llama valor absoluto al valor de dicho número despreciando su signo; así, el valor absoluto de 5 es 5, y el valor ab soluto de -3 es 3. Llamaremos a la función abs( ). La entrada a abs( ) de berá contener un número cualquiera cuyo valor absoluto deseemos calcular. La salida de la función deberá ser el mismo número exonerado de signos ne gativos. Evidentemente, podemos arreglárnoslas con la entrada utilizando un argumento. La salida, como veremos, se maneja por medio de la palabra clave return del C. Crearemos también un programa main( ) sencillo, con el pro pósito de comprobar si abs( ) se comporta como es debido; recuerde que es ta función debe ser llamada por otra. Los programas diseñados para hacer test de funciones se llaman “conductores”. El conductor toma la función y le da un par de sacudidas. Si la cosa funciona, podemos instalar nuestra nueva creación en otro programa más util con ciertas garantías. (También se aplica el término conductor [driver] a programas que controlan mecanis mos.) Nuestra particular solución es:
10 0 22
Primero, refresquemos la memoria sobre el operador condicional, ?:. El operador condicional en abs( ) funciona de la siguiente manera: si x es me nor que 0, y toma el valor -x; en caso contrario, toma el valor x. Esto es precisamente lo que necesitamos, ya que si x es -5, y será -(-5) o, lo que es lo mismo, 5. La palabra clave return hace que el valor de la expresión encerrada entre paréntesis, cualquiera que sea, quede asignada a la función que contenía di cho return. Así, cuando se llama por primera vez abs( ) en nuestro progra ma principal, abs(a) toma el valor 10, que queda asignado a la variable d. La variable y es interna de la función abs( ), pero el valor de dicha variable se comunica al programa de llamada por medio de return. El efecto de d = abs( a);
es equivalente a decir abs( a); d = y;
¿Se puede usar la segunda forma en la práctica? No, ya que el programa principal ignora la propia existencia de y. El valor devuelto puede ser asignado a una variable, como en nuestro ejem plo, o utilizado como parte de una expresión. Así, podríamos tener: resp = 2*abs ( z) + 25; pri ntf ("%d\n", abs(-32 + resp); 253
El uso de return tiene otro efecto adicional: finaliza la ejecución de la fun ción y devuelve el control a la sentencia siguiente de la función de llamada Esto ocurre incluso si la sentencia return no es la última de la función. Así, podríamos haber escrito abs( ) de la siguiente forma: /* funcion valor absoluto, segunda version */ abs(x) int(x) ;
{ if (x < 0) return (-x) ; else return (x) ;
}
Esta versión es más clara que la anterior, y no utiliza la variable adicional y. Sin embargo, desde el punto de vista del usuario, ambas versiones son la misma, ya que ambas necesitan la misma entrada y producen la misma sali da; lo único que varía es el interior de la propia función. Incluso el ejemplo siguiente tendría el mismo comportamiento: /* funcion valor absoluto, tercera version */ abs (x) int (x) ;
{
if ( x < 0) return ( — x ) ; else return ( x ) ; printf("El profesor Bonete es un petrimete.\n");
}
La sentencia printf( ) no se imprimirá en ningún caso, ya que su acceso queda impedido por las sentencias return anteriores. El profesor Bonete pue de emplear cuantas veces quiera una versión compilada de esta función en sus propios programas sin averiguar jamás la verdadera opinión que le mere ce a su ayudante. También se puede usar una sentencia como la siguiente: return;
Esta última sentencia provoca que la función que la contiene acabe su eje cución y devuelva el control a la función de llamada. Al no haber expresión alguna entre paréntesis, no se devuelve ningún valor.
Variables locales Ya hemos destacado varias veces que las variables de la función son pri vadas, y desconocidas por la función que la ha llamado. De igual forma, las variables de esta última no son conocidas por la función que ha sido llama da. Esta es la razón por la que tenemos que utilizar argumentos y return para comunicar valores a uno y otro lado. Las variables conocidas únicamente por su propia función se denominan «locales». Hasta ahora, son las únicas que hemos empleado; sin embargo, existe en C otro modo de declarar variables, de manera que puedan ser conocidas por varias funciones simultáneamente. Estas variables no locales se denominan «globales», y serán tratadas más ade lante. Entretanto, queremos dejar bien claro que las variables locales son real mente locales. Incluso en el caso de que usemos el mismo nombre para varia bles de distintas funciones, el ordenador es capaz de distinguirlas. Se puede demostrar este último punto utilizando el operador & (no confundir con el operador &&).
Localización de direcciones: el operador & El operador & devuelve la dirección en la cual se ha almacenado una va riable. Si puf es el nombre de una variable, &puf es la dirección de la misma. Podemos imaginar las direcciones como localizaciones de memoria, pero tam bién como etiquetas que utiliza el ordenador para identificar las variables. Supóngase que tenemos la sentencia. puf = 24;
Y que esta variable puf se ha almacenado en 12126. En ese caso, la sen tencia printf("%d %d\n", puf, &puf) ;
tendrá como salida 24 1212&
además, el código máquina de la primera sentencia será algo que signifique “almacénese 24 en la dirección 12126”. Vamos a utilizar este operador para comprobar dónde se almacenan va riables con el mismo nombre en diferentes funciones. /* test de localizaciones */ main() {
int puf = 24, bah = 5; 255
printf("En main(), puf = % d y &puf = % u\n", puf, &puf) ; printf("En main(), bah = % d y &bah = % u\n", bah, &bah); mikado(puf) ;
} mikado(bah) int bah;
{
int puf = 10; printf("En mikado(), puf = % d y &puf = % u\n", puf, &puf) ; printf("En mikado(), bah = % d y &bah = % u\n", bah, &bah);
} Hemos usado el formato %u (entero sin signo) para imprimir las direc ciones, por si acaso fueran mayores que el máximo tamaño permitido en en teros int. La salida de este programa en nuestro sistema es En En En En
main(), puf = main(), bah = mikado(), puf mikado(), bah
2 5 = =
y &puf = 56002 y &bah = 56004 10 y &puf = 55994 2 y &bah = 56000
¿Qué conclusión podemos sacar? Primero, que los dos pufs tienen direc ciones diferentes. Lo mismo sucede con los dos bahs. Tal como habíamos prometido, el ordenador considera que está manejando cuatro variables dis tintas. Además, la llamada mikado(puf) envió el valor (2) del parámetro (puf de main( )) al argumento formal (bah de mikado( )). Obsérvese que se trans mitió únicamente el valor. Las dos variables involucradas (puf de main() y bah de mikado( )) retienen su propia identidad. Hemos vuelto a insistir en este punto porque lo dicho no es cierto en to dos los lenguajes. En esta subrutina FORTRAN, por ejemplo, la subrutina emplea las variables del programa de llamada. La subrutina puede llamar a las variables con nombres distintos, pero las direcciones son las mismas. En C no sucede esto; cada función utiliza sus propias variables. Es más ventajo sa esta segunda opción, porque queda garantizado que el valor original de las variables no se verá alterado misteriosamente por algún efecto lateral de la función que ha sido llamada. Sin embargo, produce también algunas difi cultades, como veremos en la siguiente sección.
Alteración de variables en el programa de llamada En ocasiones se necesita que una función realice cambios en las variables de otra función diferente; por ejemplo, un problema común que surge en or denaciones de variables es proceder a su intercambio. Supongamos que tene mos dos variables llamadas x e y, y deseamos traspasar recíprocamente sus valores. La secuencia X = y;
y = x; más sencilla no funciona, ya que al ejecutarse la segunda línea, el valor origi nal de x se ha perdido. Para proceder al intercambio deberemos preparar una variable adicional, que llamaremos temp, que almacene temporalmente el valor de x antes de que se pierda: te m p = x ;
x = y; y = te m p ;
Ahora que hemos definido nuestro método de actuación, escribamos una función y un pequeño programa principal para comprobarla. Para dejar bien 257
La nueva salida es: claro qué variables pertenecen a main( ), y cuáles, a la función usaremos x e y, en la primera, y u y v en la segunda.
intercambio( ).
En principio x = 5 e y = 10. En principio u = 5 y v = 10. Ahora u = 10 y v = 5. Ahora x = 5 e y = 10.
/* cambio1 */ main()
{ int x = 5, y = 10;
}
printf("En principio x = %d e y = %d . \n", x, y); intercambia(x,y) ; printf("Ahora x = %d e y = %d.\n", x, y);
intercambia(u,v) int u,v;
{ int temp; temp =u; u
}
=
v;
Bien, no hay ningún error en intercambia( ); la función intercambia los valores de u y v. El problema es la comunicación de resultados al programa principal main( ). Tal como hemos comentado, intercambia( ) emplea va riables diferentes a las de main( ), de modo que el intercambio de los valores de u y v no produce efecto sobre x e y. ¿Podríamos usar return de alguna forma? Bien, podríamos terminar intercambia( ) con la línea return(u);
v = temp;
y cambiar la llamada en main( ) de la forma A continuación ejecutamos el programa En principio x = 5 e y = 10. Ahora x = 5 e y = 10.
¡Caramba, pero si no han cambiado! Incluyamos algunas sentencias en intercambia( ), para comprobar si hay algo mal en ella:
x = intercambia(x,y);
De este modo recuperaríamos x con su nuevo valor, pero y quedaría con denada al olvido. Con return se puede enviar al programa de llamada únicamente un valor. En este caso necesitamos comunicar dos valores. ¡Se puede hacer! Todo lo que necesitamos es emplear “punteros”. Punteros: un primer repaso
/* cambio2 */
main()
{
int x = 5, y = 10;
printf("En principio x = %d e y = %d . \n", x, y); intercambia(x,y) ; printf("Ahora x = %d e y = %d.\n", x, y);
} intercambia (u, v) i n t u , v;
{
int temp; printf("En principio u = %d y v = %d.\n", u, v) ;
temp = u; u = v;
¿Punteros?; ¿y eso qué es? En primer lugar, punteros es la traducción aceptada en el mundillo informático de la palabra inglesa pointers. Básica mente, un puntero o pointer es una representación simbólica de una direc ción. Por ejemplo, hemos usado anteriormente el operador de dirección pa ra encontrar la dirección de la variable puf. Se dice que &puf es un “puntero a puf”. En realidad, la dirección es un número (56002, en nuestro caso), y la representación simbólica &puf es una constante puntero. Después de to do, no hay nada malo en que sea constante, ya que la variable puf no va a cambiar de dirección durante la ejecución del programa. También existen en C variables puntero. Al igual que una variable de ti po char toma un carácter como valor, y una variable int un entero, la varia ble puntero toma como valor una dirección. Si damos a un determinado pun tero el nombre ptr, podremos escribir sentencias como la siguiente
v = temp; printf ("Ahora u = %d y v = %d.\n", u, v) ; }
ptr = &puf;
/* asigna la direccion de puf a ptr */ 259
Declaración de punteros
Se dice que ptr “apunta a” puf. La diferencia entre ptr y &puf es que ptr es variable, mientras que &puf es constante. Así, si lo deseamos, pode mos hacer que ptr apunte a otra dirección: ptr = &bah; /* ahora apunta a bah en vez de puf */
Ahora el valor de ptr es la dirección de bah. El operador de indirección: *
Supongamos que sabemos que ptr apunta a bah. Entonces podremos usar el operador de indirección, *, para encontrar el valor almacenado en bah. (No confunda el operador de indirección unario con el operador * binario de multiplicación.) val = *ptr; /* busca el valor al que apunta ptr */
Ya sabemos cómo declarar variables de tipo int, etc. ¿Cómo se puede de clarar una variable puntero? Podríamos suponer que es algo así: pointer ptr;
/* asi NO se declaran los punteros */
¿Por qué no? Porque no basta con saber que una variable es un puntero. También tenemos que conocer el tipo de variable a la que está apuntando dicho puntero. La razón es que las variables de tipos distintos ocupan distin tas cantidades de memoria, y existen operaciones con punteros que requieren conocer el tamaño de almacenamiento. A continuación se indica cómo se de claran, en realidad, los punteros: int *pi; char *pc;
/* pu ntero a va riable de tip o int */ /* puntero a variable tipo char */
float *pf,*pg; /* punteros a variables float
*/
El conjunto de estas dos últimas sentencias, unidas, equivaldría a val = bah;
El uso de direcciones y operadores de indirección es una forma bastante indirecta de llegar a este resultado, de ahí el nombre de “operador de indi rección”. RESUMEN: OPERADORES RELACIONADOS CON PUNTEROS
La especificación de tipo identifica el tipo de variable apuntada, y el aste risco (*) identifica a la propia variable como puntero. La declaración int *pi; está diciendo que pi es un puntero y que *pi es de tipo int. De igual forma, el valor (*pc) apuntado por pc es de tipo char. ¿Y de qué tipo es pc? Lo describiremos como de tipo “puntero a char” o “punte ro a carácter”. Su valor, como tal, es una dirección; por tanto, un entero sin signo, de forma que deberemos emplear el formato %u para imprimir un valor de pc.
I. El operador dirección:
& Seguido por un nombre de variable, da la dirección de dicha variable. Ejemplo: &fermin es II.
la dirección de la variable fermin
El operador de indirección:
/* Seguido por un puntero, da el valor almacenado en la dirección apuntada por el mismo. Ejemplo: fermin = 22; ptr = &fermin; /* puntero a fermin */ val = *ptr;
El efecto neto de estas sentencias es asignar el valor 22 a val.
Figura 9.5
Declaración y uso de punteros 261
Utilización de punteros para comunicación entre funciones
Acabamos de rozar simplemente la superficie de un mundo rico y fasci nante, el mundo de los punteros; por ahora, sin embargo, nuestro mayor in terés es utilizar punteros para resolver el problema de las comunicaciones El siguiente programa utiliza punteros para hacer que funcione (¡por fin!) la función de intercambio. Echémosle una ojeada, ejecutémosla, y seguida mente intentaremos comprender cómo funciona.
}
int x = 5, y = 10; printf('En principio x = %d e y = %d.\n", x, y); intercambia (&x, &y) ; /* envia las direcciones a la funcion */ printf("Ahora x = %d e y = %d. \n”, x, y);
}
int temp; temp = *u; *u = *v; *v = temp;
para disponer de una variable temporal que necesitamos. Deseamos almace nar el valor de x en temp, de forma que hacemos temp = *u;
temp = u; /* N O */
que almacenaría la dirección de x en lugar de su valor; estamos intentando intercambiar valores, no direcciones. De igual forma, para asignar el valor de y a x, utilizamos
intercambia(u,v) int *u, *v;
{
int temp;
Recuerde, u tiene el valor &x; por consiguiente, u apunta a x. Ello signifi ca que *u nos da el valor de x, que es lo que deseamos. Lo que no se debe hacer es
/* cambio3 */ main()
{
a continuación, en el cuerpo de la función, declararemos
/* temp toma el valor al que apunta u */
*u = *v;
que es lo mismo que x = y;
Después de todo, ¿funciona? En principio x = 5 e y = 10. Ahora x = 10 e y = 5.
Sí, funciona. Bien, veamos cómo funciona. Primero, nuestra función de llamada tiene el siguiente aspecto:
Resumiendo lo visto hasta ahora, deseábamos una función que pudiese alterar los valores de x e y; si enviamos a la función las direcciones de x e y, estamos dando a dicha función acceso a esas variables. Si usamos punte ros y el operador *, la función puede examinar los valores almacenados en dichas localizaciones y proceder a su intercambio. En general, se pueden enviar a las funciones dos tipos de información acerca de una variable. Se puede usar la forma
intercambia(&x, &y) ; funcion1(x);
En lugar de transmitir los valores de x e y estamos enviando las direccio nes; por consiguiente, los argumentos formales u y v que aparecen en intercambia (u, v)
tendrán valores de direcciones, y deberán declararse como punteros. Como x e y son enteros, u y v deberán ser punteros a enteros, de modo que declara remos int *u, *v;
que transmitirá el valor de x. Por el contrario, si usamos la forma funcion2(&x) ;
enviaremos la dirección de x. La primera forma requiere que la definición de la función incluya un argumento formal del mismo tipo que x: funcion1 (num)
int num;
263
La segunda forma, por su parte, necesita que la definición de función in cluya un argumento formal que sea un puntero al tipo de variable adecuado:
También se puede obtener el valor a partir de la dirección utilizando el ope rador *: Si hacemos pbarra = &barra, entonces podremos saber el valor alma cenado en la dirección &barra utilizando *pbarra.
funcion2 (ptr) int *ptr;
Se usa la primera forma si la función necesita el valor para hacer algún cálculo o ejecutar alguna acción. Se usa la segunda forma si la función nece sita alterar las variables del programa de llamada. Lo que hemos estado ha ciendo hasta ahora con la función scanf( ) tenía la misma finalidad. Necesi tábamos leer un valor de la variable num, y empleábamos scanf(“%d”, &num). Dicha función lee un valor y emplea la dirección que hemos dado para almacenar dicho valor. Los punteros permiten soslayar el problema de que las variables de intercambia( ) eran locales. Con ellos se puede acceder a las variables de main( ) y alterarlas en sus propias direcciones. Los usuarios de PASCAL reconocerán probablemente la primera forma como envío de valores de parámetros en PASCAL, y la segunda como envío de parámetros variables (var). Probablemente los usuarios del BASIC encon trarán el montaje un tanto complicado. Si esta sección le ha resultado algo esotérica, asegúrese de que se ejercita un poco antes de seguir adelante; con un poco de práctica llegará a encontrar los punteros sencillos, normales y con venientes.
Figura 9.6
Nombres, direcciones y valores en un sistema "direccionable por bytes”, como el IBM PC. Aunque podemos imprimir la dirección de una variable para satisfacer nuestra curiosidad, no es ése, desde luego, el uso principal del operador &. Tiene mucha más importancia el hecho de que, si empleamos &, * y punte ros podemos manipular direcciones y sus contenidos simbólicamente, co mo hicimos en el programa cambio3.
A ver cómo funcionamos
VARIABLES: NOMBRES, DIRECCIONES Y VALORES
Nuestra discusión sobre punteros se engarza en un problema más gene ral: la relación entre nombres, direcciones y valores de las variables. Am pliaremos ahora este punto. Cuando se escribe un programa pensamos en las variables como algo que contiene dos atributos: un nombre y un valor (también podríamos con siderar como atributo el tipo, por ejemplo; pero, por el momento, no nos concierne). Una vez compilado y cargado el programa, el compilador supo ne también dos atributos a las variables: su valor y su dirección. La direc ción es la versión en ordenador del nombre de la variable. En muchos lenguajes la dirección concierne exclusivamente al ordena dor, siendo inasequible al programador. En C, sin embargo, se pueden co nocer y emplear las direcciones a través del operador &: &barra es la dirección de la variable barra
Podemos obtener el valor de una variable empleando su nombre: printf(“%d \ n’\ barra) imprime el valor de barra.
Ahora que ya sabemos algo sobre las funciones, podemos intentar apli car nuestros conocimientos a la realización de algunos programas útiles. Vea mos... ¿qué se puede hacer? ¿Qué tal una función potencial, algo que permite elevar 2 a la 5.a poten cia o 3 a la 3.a? En primer lugar, debemos decidir qué tipo de entrada necesi ta el programa. Está claro; necesitamos enviar el número y el exponente. Po demos manejar estos dos valores con dos argumentos pot(base, exp) int base, exp;
(Por ahora nos limitaremos a enteros y a números relativamente pequeños.) A continuación debemos decidir la salida. También, en este caso, es ob via. Necesitamos un solo número como salida, la respuesta. Esto podría ex presarse como return ( respuesta) ; 265
Ahora debemos decidir el algoritmo a emplear para calcular la respuesta hacer respuesta igual a 1 multiplicar la respuesta por la base tantas veces como indique exp Quizá no quede demasiado claro cómo realizar la segunda etapa, por lo que la dividiremos en dos partes: Multiplicar respuesta por base y decrementar exp 1 Detenerse cuando exp llegue a 0. Si exp fuese 3, por ejemplo, conseguiríamos efectuar 3 multiplicaciones, lo que parece bastante razonable. Bien, pongámoslo en código C. /* calculo de potencias */ pot(base, exp) int base, exp;
Bien, 2 a la 3.a potencia es 8, y -3 a la 3.a potencia es -27. Hasta ahora vamos bien. Sin embargo, 4 elevado a -2 es 1/16, no 1; y 5 elevado a la 10 potencia es 9765625, si la memoria no nos falla. ¿Qué ha pasado? En primer lugar, que el programa no estaba diseñado para manejar potencias negativas, de forma que se ha “estrellado” con ese problema. Además, al haber empleado el tipo int, nuestro sistema tolera úni camente números inferiores a 65535. Podemos arreglar el programa incluyendo procesado de números negati vos y utilizando números de puntos flotantes para la base y la respuesta. El exponente, sin embargo, deberá seguir siendo entero, ya que es el número de veces que hemos de multiplicar; no se pueden efectuar 2,31 multiplicacio nes. /* calculo de potencias */ double pot(base, exp) double base; int exp;
{
double respuesta;
■{
int respuesta;
if (exp > 0)
for (respuesta = 1; exp > 0; exp--) respuesta = respuesta * base; return(respuesta);
}
for (respuesta = 1.0; exp > 0; exp--) respuesta *= base; return(respuesta); }
else if (base != 0)
{
Y ahora comprobemos la función con un driver.
for (respuesta = 1.0; exp < 0; exp++) respuesta /= base; return(respuesta);
/* test de potencia */ main()
}
else /* base = 0 y exp <= 0 */
{
{
int x;
}
x = pot (2, 3) ; printf("%d\n", x); x = pot (-3, 3) ; printf("%d\n", x) ; x = pot (4, -2) ; printf("%d\n”, x); x = pot(5,10); printf("%d\n", x) ;
Unimos las dos funciones, las compilamos y las ejecutamos. Obtenemos el siguiente resultado: 8 -27
1
761
printf("0 a la potencia %d no esta permitido! \n", exp); }
Hay algunas novedades que anotar. La que primero salta a la vista es que hemos declarado el tipo de la función. Como respuesta es de tipo double, pot( ) también debe ser de tipo double, ya que pot queda asignada en el va lor devuelto por return. Bien, y ¿por qué no hemos declarado antes las fun ciones? La respuesta es que las funciones C se suponen de tipo int (la mayor parte lo son) a menos que se indique lo contrario. También dejamos claro que no se nos han olvidado los nuevos operado res de asignación que vimos en el capítulo 8. En tercer lugar, hemos adaptado las potencias negativas a divisiones, tal como permiten las leyes del álgebra. Así ha surgido un problema adicional, la división por 0, que se ha evitado por medio de un mensaje de error. Devol vemos el valor 0 para que el programa no se detenga. 267
Se puede utilizar el mismo programa principal siempre y cuando declare mos también allí el tipo de pot( ). /* test de potencia */ main()
{
int x; x = pot (2. 0, 3) ; printf ( "%.0f \n", x); x = pot(-3.0,3); printf("%.0f\n", x); x = pot (4.0, -2) ; printf("%.4f\n", x); x = pot (5. 0, 10) ; printf("%.0f\n", x);
claración se incluirán el nombre de la función y los paréntesis (sin ar gumento), con el fin de identificar dicho nombre como función. main()
{ char rch, pun(); float plaf () ;
}
No lo olvide. Si una función devuelve un valor no entero, declare el tipo de función allá donde se define y donde se usa. RESUMEN: FUNCIONES
} I. Formatos: Una definición típica de función tiene el siguiente formato:
Esta vez la salida es de los más satisfactorio a -27 0.0625 9765625
Este ejemplo sugiere que incluyamos en la explicación nuestra próxima sección.
especificar tipos de funciones El tipo de una función queda determinado por el tipo de valor que de vuelve, no por el tipo de sus argumentos. Las funciones se suponen de tipo int, a menos que se indique lo contrario. Si una función no es de tipo int. se deberá advertir en dos sitios: 1. Al declarar el tipo de función en su definición: char pun(ch, n) /* función que devuelve un caracter */ int n; char ch; float plaf(num) /* funcion que devuelve un numero float *f int num;
2. Al declarar el tipo de la función en la función de llamada. Esta decla ración debe incluirse junto con las declaraciones de variables; en la de
nombre (listas de declaración de los cuerpo de la función
argumentos) argumentos
La presencia de la lista de argumentos y las declaraciones es opcional. Si se emplean variables distintas a los propios argumentos, deberán declararse dentro del cuerpo de la función que está comprendido entre las llaves. Ejemplo: dif(x,y) /* nombre de la funcion y argumentos */ int x.y; /* declaracion de argumentos */ { /* comienza cuerpo de la funcion */ int z ; z = x - y; return (z) : } /* fin del cuerpo de la funcion */
II. Comunicación de valores: Para enviar valores desde el programa de llamada a la función se emplean argumentos. Si las variables a y b tienen los valores 5 y 2, la llamada c = dif(a,b);
transmite los valores 5 y 2 a las variables x e y . Los valores 5 y 2 se llaman argumentos efectivos o parámetros, y las variables x e y de la función dif se denominan argumentos formales. La palabra clave return devuelve un valor desde la función al programa de llamada. En nuestro ejemplo, c recibiría el valor de z, que es 3. Como norma general, una función no tiene efectos sobre la variable del pro grama de llamada. Si se desea afectar dichas variables, utilícense punteros
269
como argumentos. Este sistema será necesario cada vez que se desee comu nicar más de un valor de retorno desde la función al programa de llamada. III. Tipo de funciones:
Las funciones deben ser del mismo tipo que el valor que devuelve. Las fun ciones no declaradas se suponen del tipo int. Cuando una función sea de otro tipo, deberá ser declarada tanto en la propia función como en el pro grama de llamada.
}
printf(“Pulse un caracter cualquiera- Se detiene con Q.\n"); ch = getchar(); printf(“Aja! eso era una %c!\n“,ch); if (ch != 'Q') mas();
mas ()
{
main() ;
}
Ejemplo: main() {
float q, x, puff(); /* declaracion en el programa de 1lamada/* int n; q = puff(x,n);
}
float puff(u, k) /* declaracion en la definicion de función */ float u; int k;
¡La función main( ) llama a mas( ), y mas( ), a su vez, llama a main( )! Cuando main( ) es llamada por mas( ), comienza desde el principio; de este modo, creamos un bucle bastante sinuoso. De hecho, una función puede llamarse a sí misma. Podemos simplificar el ejemplo anterior de la siguiente forma: /* que trabaje main el doble */ #include main()
{
char ch;
float toro;
printf("Pulse un caracter cualquiera. Se detiene con Q.\n“); ch = getchar(); printf(“Aja! eso era una %c!\n",ch); if (ch != 'Q')
return(toro);
/* devuelve un valor float */
}
}
{
main();
Hagamos una ejecución sencilla para ver cómo funciona. Obsérvese que incluso el carácter nueva línea se transmite cuando se utiliza la tecla [enter].
las funciones C se crean de la misma manera
Pulse un caracter cualquiera. Se detiene con Q.
I
En C todas las funciones del programa tienen la misma estructura. Cada una puede llamar a otra función, o ser llamada por otra. En esto se diferen cia el C de los procedures PASCAL, ya que en PASCAL unos procedures pueden estar anidados dentro de otros. Un procedure anidado ignorará cual quier otro procedure anidado en otro sitio. ¿La función main( ) tiene algo de especial? Sí, es algo especial en el senti do de que cuando un programa consta de varias funciones, la ejecución co mienza precisamente con la primera sentencia de main( ). Exceptuando esta diferencia, main( ) es una función como las demás; incluso puede ser llama da por otras funciones, como muestra el siguiente ejemplo:
/ * que trabaj e mai n */ #i nc l ude
main()
{
char ch;
Aja! eso era una I! Pulse un caracter cualquiera. Se detiene con Q. Aja! eso era una
!
Pulse un caracter cualquiera. Se detiene con Q. Q
Aja! eso era una Q!
El proceso por el que una función se llama a sí misma recibe el nombre de “recursion”. Un bucle establecido por recursión no funciona de la misma forma que los bucles while o do while. Cuando main( ) se llama a sí misma, no se dirige en realidad al principio exacto. En su lugar se produce un nuevo conjunto de variables de main( ). Si hacemos imprimir las direcciones de una variable en un bucle ordinario, la dirección no cambiará de iteración a itera ción. Con el tipo de bucle aquí creado, la dirección cambia, ya que se crea un nuevo ch cada vez. Si el programa realiza el bucle 20 veces, habrá 20 nue vas variables creadas, todas ellas llamadas ch, pero cada una en una direc ción distinta. 271
Resumen COMPILACION DE PROGRAMAS CON DOS O MAS FUNCIONES
La manera más simple de utilizar varias funciones es colocar todas ellas en el mismo fichero. En ese caso, la compilación se realiza de la misma for ma que la compilación de una sola función. Otra manera es utilizar la orden de preprocesador #include. Si una fun ción está en el fichero fich1.c, y la segunda, en el fichero fich2.c, se habrá de incluir la siguiente orden en el fichero fich1.c:
Hemos empleado funciones como piezas para crear un programa mayor. Cada función deberá tener un propósito único y definido. Hemos usado ar gumentos para comunicar valores a la función, y la palabra clave return para enviar valores de vuelta de la función al programa de llamada. Si el valor devuelto por la función no es de tipo int, se deberá especificar el tipo de la función tanto en la definición de la función como en la sección de declara ción del programa de llamada. Si se desea que la función afecte a variables del programa de llamada, se deberán emplear direcciones y punteros.
#include "fich2.c"
Para más información sobre #include, véase capítulo 11. Otras maneras de abordar el problema dependen bastante del sistema empleado. Algunas de ellas pueden ser: UNIX Supongamos que fich1.c y fich2.c son dos ficheros que contienen fun ciones C; en ese caso, el comando cc fich1.c fich2.c
compilará ambos ficheros y producirá un fichero ejecutable llamado a.out. Además, se producirán los dos ficheros objeto llamados fich1.o y fich2.o. Si posteriormente se altera fich1.c, y no fich2.c, se puede compilar el pri mero y combinarlo con la versión en código objeto del segundo fichero, uti lizando el comando
Hasta ahora hemos aprendido Cómo definir una función. Cómo comunicar información a una función: utilizando argumentos. La diferencia entre argumentos formales y argumentos específicos o pa rámetros: los primeros son variables utilizados por la función; los segundos, el valor enviado desde el programa de llamada. Dónde se declaran los argumentos: después del nombre de la función y antes de la primera llave. Dónde se declaran las demás variables locales: después de la llave de abrir. Dónde y cómo se usa return. Dónde y cómo se usan direcciones y punteros para comunicación.
cc fich1.c fich2.o
Lattice C y Microsoft C
Compílese fich1.c y fich2.c por separado, produciendo dos ficheros en código objeto fich1.obj y fich2.obj. Utilice a continuación el linker para combinarlos junto con los módulos objeto estándar de c.obj: link c fich1 fich2
Sistemas basados en Código Ensamblador
Algunos de ellos permiten compilar varios ficheros a la vez al estilo UNIX:
Cuestiones y respuestas Cuestiones
1. Escriba una función que devuelva la suma de dos enteros. 2. Indicar los cambios, si los hay, que se deben hacer en la función de la cuestión 1 para conseguir que se sumen dos números de tipo float. 3. Diseñar una función, alter( ), que tome dos variables de tipo int, x e y, y las trans forme en su suma y diferencia, respectivamente. 4. ¿Hay algo incorrecto en la siguiente definición de función?
cc fich1.c fich2.c s o l e (num)
o de alguna otra forma equivalente. En otros casos, deberá producir módu los de código ensamblado por separado y combinarlos en el propio proceso de ensamblaje.
int num, cont; { for (cont = 1; cont <= num; num++) printf(" O sole mio!\n") ;
} 273
Respuestas 1. suma (j,k) int j, k;
{
return (j + k); }
2. float suma (j,k) float j, k;
También se debe declarar float suma( ) en el programa de llamada 3. Como deseamos alterar las variables del programa de llamada, deberemos usar direcciones y punteros; la llamada deberá ser alter (&x,&y) Una posible solución sería alter (px,py) int *px, *py; /* punteros a x e y */ {
int sum, dif; sum dif *px *py
= = = =
*px + *py; /* suma contenidos de ambas direcciones *px - *py; sum; dif;
*/
} 4. Sí, num debe declararse antes de la primera llave, no después. Además, el bucle debe incluir cont + + , no num + + .
Ejercicios 1. Escriba una función max(x,y) que devuelva el mayor de dos valores. Diseñe la función chlinea(ch,i,j) que imprima el carácter requerido entre las co lumnas i y j. Véase el programa de caricaturas del capítulo 7.
2.
10 Modos de almacenamiento y desarrollo de programas En este capítulo encontrará: • Modos de almacenamiento: pro pósito • Variables automáticas • Variables externas • Variables estáticas • Funciones estáticas externas • Variables en registros • ¿Qué modo de almacena miento empleamos? • Una función de números aleatorios • Lanza los dados • Una función para atrapar enteros:
getint( ) • Planteamiento • Flujo de información para
getint( ) • El interior de getint( )
• Conversión de string a ente ro: stoi( ) • ¿Por qué no las probamos? • Ordenación de números • Lectura de datos numéricos • Elección de la representa ción de datos • Final de la entrada • Otros aspectos • Main( ) y getarray( ) • Explicación • Ordenación de los datos • Impresión de los datos • Resultados • Repaso • Hasta ahora hemos apren dido • Cuestiones y respuestas • Ejercicios
277
Modos de almacenamiento y desarrollo de programas CONCEPTOS Variables locales y globales Modos de almacenamiento Función de números aleatorios Comprobación de errores Programación modular Ordenación
PALABRAS CLAVE auto, extern, static, register
una serie de funciones útiles; conforme vayamos haciéndolo, trataremos de demostrar algunas de las consideraciones que hay que tener en cuenta a la hora de diseñar una función. En concreto, haremos énfasis en la importan cia de la programación modular, que permitirá subdividir nuestro trabajo en tareas manejables. Pero, como hemos prometido, trataremos primero los modos de almace namiento.
Modos de almacenamiento: propósito Ya hemos comentado con anterioridad que las variables locales son co nocidas únicamente por las funciones que las contienen. En C se ofrece tam bién la posibilidad de trabajar con variables globales conocidas por varias funciones. Supongamos, por ejemplo, que deseamos que la variable unida des sea conocida por dos funciones, main( ) y crítica( ). Lo que tenemos que hacer es asignar a unidades el modo de almacena miento “externo” (extern), como se puede ver: /* unidades como global */ int unidades; / * una variable externa */ main() { extern int unidades; printf("Cuantos reburcios hay en una drumera de harina?\n"); scanf("%d", &unidades) ; while (unidades != 3419) critica(); printf("Seguro que has mirado! ! !\n") ; } critica() { extern int unidades; printf("Lo siento, chaval. Prueba otra vez.\n"); scanf("%d", &unidades) ; }
La salida podría ser: Una de las razones por las que el C es tan potente es por permitir contro lar hasta los más mínimos detalles del programa. Los modos de almacena miento que se ofrecen en C son un ejemplo de este tipo de control, ya que permiten determinar qué funciones conocen, qué variables y hasta cuándo va a permanecer una variable en un programa. Los modos de almacenamien to constituirán el motivo de la primera parte de este capítulo. Por otro lado, aprender a programar no es simplemente conocer las re glas del lenguaje, al igual que escribir una novela (o incluso una carta), es algo más que saber las reglas del español. En este capítulo desarrollaremos
Cuantos reburcios hay en una drumera de harina? 14
Lo siento, chaval. Prueba otra vez 3419 Seguro que has mirado!!!
(La verdad es que sí.) Obsérvese que la variable unidades ha sido leída por la función crítica( ) la segunda vez; pero, a pesar de ello, también main( ) la conocía, y la ha utilizado para abandonar el bucle while. 279
Hemos conseguido que unidades fuese una variable externa definiéndola antes de cualquier función (es decir, externamente a ella). A continuación, dentro de la función que vaya a utilizar esa variable, volvemos a declarar la misma anteponiendo al tipo de variable la palabra clave extern. Esta palabra clave informa al ordenador que debe buscar la definición de la variable fuera de la función. Si hubiésemos omitido la palabra (extern) en, digamos, crítica( ), el ordenador habría considerado en esta función que existía una varia ble distinta con el mismo nombre unidades, pero local y, por consiguiente, limitada a la función crítica( ). En tal caso, la otra variable unidades (inclui da en main( )) no hubiese cambiado su valor tras la ejecución del scanf( ) ejecutado en crítica( ). Ya sabíamos que cada variable tiene su tipo. Además, cada variable tiene un modo de almacenamiento. Existen cuatro palabras clave en C que se em plean para describir modos de almacenamiento: extern (por externa), auto (automática), static y register. Hasta ahora no nos habíamos ocupado de los modos de almacenamiento porque las variables declaradas en una función se supone que son de modo auto, a menos que se indique lo contrario (o sea, son automáticas automáticamente). El modo de almacenamiento de una variable queda determinado por el lugar donde se define y la palabra clave empleada, suponiendo que se use alguna. El modo de almacenamiento es responsable de dos propiedades distintas. Primero, controla las funciones a las que dicha variable es accesible. Se lla ma “alcance” de una variable a la mayor o menor extensión de la accesibili dad de la misma. En segundo lugar, el modo de almacenamiento determina cuánto tiempo va a persistir una variable en memoria. Pasemos a estudiar cada uno de estos modos por separado. Variables automáticas
Todas las variables declaradas en una función, por defecto, son automá ticas. No obstante, si deseamos dejar bien clara nuestra intención de que la variable sea automática, podemos emplear la palabra clave auto: ma i n ( )
{ auto i nt pl of;
Por ejemplo, conviene seguir este sistema para mostrar que intenciona damente hemos evitado una definición de función externa. Las variables auto máticas tienen alcance local. La única función que conoce una variable de este tipo es aquella donde se ha definido. (Por supuesto, se pueden usar ar gumentos para comunicar el valor y dirección de una variable de este tipo a otra función, pero convendrá con nosotros que este conocimiento será siem pre parcial e indirecto.) Como consecuencia de lo anterior, otras funciones pueden utilizar variables con el mismo nombre, las que se tratarán como va riables independientes almacenadas en diferentes localizaciones de memoria.
Una variable automática abre los ojos al mundo cuando se llama a la fun ción que la contiene. Cuando esta función acaba su tarea y devuelve el con trol al programa de llamada, la variable queda relegada al olvido. Su locali zación de memoria se empleará en adelante para otros usos. Un detalle más sobre el alcance de las variables automáticas: su alcance queda confinado al bloque (par de llaves) en el cual se ha declarado la varia ble. Siempre podemos declarar nuestras variables al comienzo del bloque de la función, de manera que el alcance sea la función completa. Pero, en prin cipio, uno puede también declarar una variable dentro de un sub-bloque. En ese caso, la variable sería conocida únicamente dentro de la subsección de la función. En circunstancias normales no se suele hacer uso de esta opción, pero hay pocas cosas que un programador que se sienta acosado no sea ca paz de intentar. Variables externas
Cuando una variable se define fuera de una función, se dice que es exter na. Dicha variable externa puede también ser declarada dentro de la función que la emplea utilizando la palabra clave extern. En tal caso, la declaración tendría un aspecto como el siguiente: int errumpir; /* 3 variables definidas externamente */ char cuteria; double blanca; main() { extern int errumpir; / * declaracion de que existen 3 variables*/ extern char cuteria; / * definidas externamente* / extern double blanca;
Se puede omitir por completo el grupo de declaraciones extern si las defi niciones originales aparecen en el mismo fichero y antes de la función que las utiliza. Sin embargo, el uso de la palabra clave extern permite que una función emplee una variable externa que haya sido definida después de la fun ción en el mismo fichero, o incluso en un fichero diferente. (Por supuesto, ambos ficheros deberán compilarse, unirse (link o ensamblarse a la vez.) Cuando se omite la palabra extern en la declaración de la variable en una función, se crea una nueva variable distinta y automática con el mismo nom bre. Conviene en estos casos etiquetar esta segunda variable con la palabra “auto”, para dejar claro que se ha hecho intencionadamente y no por des piste. En los siguientes ejemplos se muestran las cuatro combinaciones posibles: /* Ejemplo 1 */ int abracadabra; main()
{
extern int abracadabra; /* se declara como externa */
281
Variables estáticas
} magia()
{
extern int abracadabra;
} Aquí aparece una variable externa abracadabra, conocida tanto por main( ) como por magia( ). /* Ejemplo 2 */ int abracadabra; main()
{
El nombre puede parecer una contradicción, como si fuese una variable que no puede variar. En realidad, la palabra “estática” se refiere a que estas variables quedan en memoria. El alcance de las variables estáticas es el mis mo que el de las variables automáticas; pero, a diferencia de ellas, no desa parecen cuando la función que las contiene finaliza su trabajo. El ordenador recuerda sus valores, y permanecerán allí si la función vuelve a ser llamada otra vez. En el siguiente ejemplo se demuestra este punto, y se indica cómo declarar una variable “static”. /* variable estatica */ main()
extern int abracadabra; /* se declara como externa */
{
int cont;
}
for (cont = 1; cont <= 3; cont++)
magia()
/* abracadabra no se declara de ninguna forma
{
printf("Aqui llega iteracion %d:\n", cont); pruebastat();
*/
}
}
}
pruebastat()
{ De nuevo existe una variable externa abracadabra conocida por ambas funciones. Esta vez, sin embargo, magia( ) conoce la variable por defecto. /* Ejemplo 3 * / int abracadabra; main() { int abracadabra; / * al declararse queda como auto por defecto*/ } magia() { auto int abracadabra; }
En este caso se han creado tres variables distintas. El abracadabra de main( ) es automático por defecto, y, por tanto, local a main. Esta misma variable es automática, porque se ha indicado explícitamente así en magia( ), y queda confinada a esta función. La variable externa abracadabra no se co noce ni en main( ) ni en magia( ), pero podría ser conocida por otra función del fichero en la que no se hubiese declarado localmente abracadabra. Estos ejemplos demuestran el alcance de las variables externas. Permane cen en el ordenador durante toda la ejecución del programa, y, al no perte necer a ninguna función en concreto, no pueden eliminarse al acabar ningu na de ellas.
int muere = 1; static int vive = 1;
}
printf("muere = %d y vive = %d\n", muere++, vive++) ;
Observe que pruebastat( ) incrementa cada variable tras haber impreso su valor. La ejecución de este programa da la siguiente salida: Aqui llega iteracion 1: muere = 1 y v i v e = 1 A qui llega iteracion 2: muere = 1 y vive = 2 Aqui llega iteracion 3: muere = 1 y vive = 3
La variable estática vive recuerda que su valor se incrementó en 1, mien tras que la variable muere resucita y fallece cada vez que se ejecuta la fun ción. Este último punto establece una diferencia en la inicialización: muere se inicializa cada vez que se llama pruebastat( ), en tanto que vive se inicializa una sola vez, cuando se compila pruebastat( ). Funciones estáticas externas
Se puede declarar también una variable static externamente a las funcio nes. El resultado es la creación de una función “estática externa”. La dife rencia entre una variable externa ordinaria y una variable externa estática re side en su alcance. La variable externa ordinaria se puede utilizar en funcio283
nes de cualquier fichero, mientras que la variable estática externa puede em plearse únicamente en funciones del mismo fichero, y situadas debajo de la definición de la variable. Se puede conseguir una variable estática externa colocando su definición fuera de la función: static arco = 1; circulo()
Hemos dicho “con un poco de suerte’’ porque la declaración de una va riable como modo registro es más una súplica que una orden directa. El or denador intentará atender a sus demandas, pero tiene también que preocu parse del número de registros que hay disponibles, que suele ser bastante es caso; por tanto, es posible que no se pueda atender a su requerimiento. En tal circunstancia, la variable se transforma en una variable automática ordi naria.
{
Dentro de poco contemplaremos un ejemplo en el cual se necesita este tipo de variable. Los ficheros 1 y 2 se compilan conjuntamente
¿Qué modo de almacenamiento empleamos?
La respuesta a esta pregunta es casi siempre “automático”. Después de todo, ¿por qué si no se ha escogido como opción por defecto? Sí, sabemos que a primera vista el almacenamiento externo es bastante seductor. Hágan se externas todas las variables, y uno no tendrá que volver a preocuparse de utilizar argumentos, punteros y comunicaciones entre funciones y toda la parafernalia restante. Desgraciadamente, tendríamos que empezar a preocupar nos porque la función A ha cambiado subrepticiamente las variables de la función B, lo que no entraba en absoluto en nuestros planes. La evidencia incuestionable de muchos años de experiencia colectiva en ordenadores es que el segundo peligro supera con mucho los superficiales encantos de un empleo masivo de almacenamiento externo. Una de las reglas de oro de una programación protectora es observar el principio “cada uno sabe lo que necesita saber y nada más”. Mantenga las tareas de cada función tan privadas como pueda, compartiendo el mínimo número posible de valores y variables con otras funciones. Habrá también ocasiones en que los restantes modos sean útiles, por eso están ahí. Sin embargo, aconsejamos que se pregunte a sí mismo si realmente necesita usarlos antes de emprender una aventura con ellos.
Se conoce tim en main ( ), figaro ( ), verde ( ) y rojo ( ) Se conoce tum únicamente en main ( ) y fígaro ( )
Figura 10.1
Variables externas y externas static Variables registro Normalmente, las variables se almacenan en la memoria del ordenador. Con un poco de suerte, las variables de modo de almacenamiento registro quedan almacenadas en los registros de la CPU, en donde son mucho más accesibles, y se manipulan más rápidamente que en memoria. Por lo demás, las variables registro son idénticas a las variables automáticas. Se organizan de la siguiente forma: main() register int rapido;
RESUMEN: MODOS DE ALMACENAMIENTO I. Palabras clave: auto, external, static, register II. Comentarios generales:
Los modos de almacenamiento de una variable determinan su alcance y el tiem po que permanece la variable en el ordenador. El modo de almacenamiento queda, a su vez, determinado por el lugar donde se define la variable y por la palabra clave asociada que se incluya. Si una variable se define fuera de una función, se clasifica como externa y tiene alcance global. Las variables que se declaran dentro de la función son automáticas y locales, a menos que se em plee alguna de las palabras clave restantes. Si una variable externa se define con anterioridad a una función, esta última es capaz de reconocerla, aunque no se declare internamente. 285
III. Propiedades MODO DE ALMACENAMIENTO
PALABRA CLAVE
automático registro estático
auto register static
externo
extern
DURACION
temporal temporal persistente
ALCANCE
local local local
persistente
global (a todos los ficheros) static persistente externo estático global (a un fichero) Los modos situados por encima de la línea de puntos se declaran dentro de una función. Los situados por debajo de la línea se definen fuera de la función.
Veamos ahora un ejemplo de función que emplea una variable estática externa.
Función de números aleatorios No se puede vivir sin una función que genere números aleatorios. Cuan do alguien le pida que piense un número, podrá dirigirse a esta poderosa fuente en lugar de balbucear una súplica para que le concedan tiempo. De la misma importancia, pero quizá menos drástica, sea su utilización en muchos juegos de ordenador. En realidad, vamos a estudiar un “generador de números seudoaleatorios”. Con ello queremos decir que la secuencia de números que se va a obte ner es predecible (los ordenadores no se caracterizan por su espontaneidad); pero, en cualquier caso, están razonablemente repartidos con uniformidad en el rango posible de valores. El planteamiento comienza con un número que se llama “semilla”. Se usa la semilla para producir un nuevo número, el cual, a su vez, se utilizará como nueva semilla. La nueva semilla, por su parte, puede emplearse para producir otra nueva semilla, y así sucesivamente. Por todo lo dicho, si que remos que el esquema se comporte adecuadamente, la función deberá recor dar la semilla que utilizó la última vez que fue llamada. ¡Ajajá! Aquí hace falta una variable estática. A continuación presentamos la versión 1. (Sí, la versión 2 viene en segui da.) /* aleat1 */ aleat() {
La variable estática azar comienza con el valor 1 y queda alterada por la fórmula mágica cada vez que se solicitan los servicios de la función. El resultado en nuestro sistema es un número situado en algún lugar del rango — 32768 a 32767. Los sistemas que tengan un tamaño diferente de números enteros (int) producirán resultados distintos. Comprobaremos el funcionamiento de nuestro generador de números alea torios con un sencillo driver. /* prueba aleat1 */ main( ) {
int cont;
}
for (cont =1; cont <= 5; cont++) printf("%d\n", aleat ());
La salida obtenida es: -26514 -4449 20196 -20531 3882
Bien, parece bastante aleatorio. Vamos a ejecutarlo de nuevo. Esta vez el resultado obtenido es: -26514 -4449 20196 -20531 3882
¿Dónde he visto yo antes esta secuencia? Bueno, ya avisábamos que este generador era “seudo”. Cada vez que el programa principal se ejecuta, se comienza con la misma semilla, 1. Podemos vadear el problema introduciendo una segunda función, saleat( ), que permita reinicializar la semilla. El truco consiste en hacer que azar sea una variable estática externa conocida únicamente por aleat( ) y saleat( ). Mantenga estas dos funciones en su propio fichero, y compílelo por separa do. La modificación a introducir es la siguiente:
static int azar = 1; azar = (azar * 25173 + 13849) % 65536; /* formula magica */
}
return (azar);
/* fichero para aleat () y saleat() */ static int azar = 1; aleat()
{
287
}
azar = (azar * 25173 + 13849) % 65536; return (azar);
/* formula magica */
EL ORDENADOR PERSONAL PARA...
saleat(x)
unsigned x; { azar = x;
}
Utilice el siguiente programa principal: /* prueba aleat2
UNA ESTRELLA DE CINE
UN FUNCIONARIO DE PRISIONES
UN VOYEUR
UN PELUQUERO
*/
main()
{ int cont; int semilla;
}
printf("Elija un numero como semilla.\n"); saleat(semilla); /* pone una nueva semilla */ for (cont = 1; cont <= 5; cont++) printf("%d\n", aleat());
Ejecutemos el programa una vez: Elija un numero como semilla. 1 -26514 -4449 20196 -20531 3882
Si usamos un valor de 1 para semilla, obtenemos los mismos valores que anteriormente. Probemos ahora con el valor 2: Elija un numero como semilla. 23832 20241 -1858 -30417 -16204
¡Muy bien! Hemos conseguido un conjunto de números diferente. Desa rrollemos ahora una aplicación útil para nuestras funciones.
Lanza los dados Vamos a intentar simular un juego de azar muy popular, el lanzamiento de dados. Los dados más corrientes, con mucho, tienen seis caras; prepara remos un programa que emplee dos de estos dados. No olvidemos, sin em bargo, que hay muchas otras posibilidades: en bastantes juegos de aventuras y fantasía se usan dados con la forma de cualquiera de los cinco poliedros regulares: es decir, con 4, 6, 8, 12 ó 20 caras. (Fueron los antiguos griegos quienes demostraron que existen cinco únicos cuerpos geométricos regula res, con todas las caras de la misma forma y tamaño; estos poliedros son la base de todas las variedades de dados. Se podrían construir dados con otro número de caras, pero las posibilidades de que saliese un número determina do no serían las mismas para todos ellos.) Sin embargo, los cálculos en el ordenador no están limitados por consi deraciones geométricas, por lo que podemos diseñar un dado electrónico con cualquier número de caras que deseemos. Empezaremos con 6 caras y gene ralizaremos después. Lo que deseamos es un número aleatorio entre 1 y 6, pero nuestra función aleat( ) produce hasta ahora un número aleatorio en el rango -32768 a 32767, por lo que deberemos hacerle algunos pequeños ajustes. Una posibilidad podría ser: 1. Dividir el número aleatorio por 32768. El resultado sería un número x en el rango -1 < =x < 1. (Previamente tendremos que haberlo con289
La salida sería: 2. 3. 4. 5. 6. 7.
vertido a tipo float, para que se puedan contener fracciones decima les.) Sumar 1. El número ahora satisface la relación 0 < =x < 2. Dividir por 2. Ahora 0 < =x < 1. Multiplicar por 6. En este caso 0 < =x < 6. (Bastante cerca, pero el 0 no sirve como valor.) Sumar 1. En este momento 1 < = x < 7. (Nota: todavía tenemos frac ción decimal.) Truncar a entero. Ahora tendremos el entero en el rango 1 a 6. Para generalizar, basta con reemplazar el 6 de la etapa 4 con el núme ro de caras deseado.
Introduzca semilla. 1
Indique numero de caras por dado; 0 para terminar.
6
Cuantos dados? 2
Acaba de sacar un 4 con 2 dados de 6 caras. Cuantas caras? Pulse 0 para acabar. 6
Cuantos dados? 2
Acaba de sacar un 7 con 2 dados de 6 caras. Cuantas caras? Pulse 0 para acabar. 0
La función que realiza estas etapas es: /* juega a los dados */ #define ESCALA 32768.0 cubilete(lados) float lados; { float tirada; tirada = ( (float) aleat() /ESCALA + 1.0) * lados / 2.0 + 1.0; return ( (int) tirada); }
Hemos incluido explícitamente dos moldeadores de tipo con el fin de re marcar las conversiones de tipo que están teniendo lugar. Hagamos ahora un programa para jugar a los dados: /* tiradas multiples de dados */ main() { int dados, cont, tirada, semilla; float lados; printf("Introduzca semilla.\n"); scanf("%d", &semilla); saleat(semilla); printf("Indique numero de caras por dado; 0 para terminar.\n"); scanf("%f", &lados); while (lados > 0) { printf("Cuantos dados?\n"); scanf("%d", &dados); for (tirada = 0, cont = 1; cont <= dados; cont++) tirada += cubilete(lados);/* calcula total de la tirada */ printf ("Acaba de sacar un %d con %d dados de %.0f caras. \n", tirada, dados, lados); printf("Cuantas caras? Pulse 0 para acabar.\n"); scanf("%f", &lados); } printf("QUE TENGA SUERTE !!\n"); }
QUE TENGA SUERTE!!!
Gracias. Se puede utilizar cubilete( ) de muchas formas diferentes. Si se hace la dos igual a dos, la función simulará el lanzamiento de una moneda con “ca ra” = 2 y “cruz” = 1 (o viceversa, si realmente lo prefiere). También es muy fácil modificar el programa para mostrar los resultados de cada uno de los dados, además del total. Si necesita un mayor número de tiradas (un ima ginario dueño del castillo barajando atributos de caracteres), se puede modi ficar fácilmente el programa para producir una salida como la siguiente: Introduzca semilla 10
Introduzca numero de tiradas; 0 para terminar. 18 Cuantas caras y cuantos dados? 6 3 Las 18 puntuaciones conseguidas con 3 dados de 6 caras son: 7 5 9 7 12 10 7 12 10 14 9 8 13 9 10 7 16 10 Cuantas tiradas? Pulse 0 para acabar.
0
Otro posible empleo de aleat( ) (pero no de cubilete( )) podría ser modi ficar nuestro viejo amigo el programa de adivinar números de forma que fuese el ordenador el que escogiera y usted el que intentase adivinar, en lugar de lo contrario. Dediquémonos ahora a otras funciones. Nuestro siguiente proyecto es el diseño de una función que sea capaz de leer enteros.
Una función para atrapar enteros: getint( ) Quizá le parezca un proyecto demasiado poco ambicioso. Después de to do, podemos emplear scanf( ) con el formato % d si queremos leer un ente291
ro. Indudablemente, sería una solución muy fácil (incluso perezosa), pero tiene un grave defecto. Si se teclea por error, digamos, una T en lugar de un 6, scanf( ) intentará interpretar la T como un entero. Lo que deseamos es dise ñar una función que estudie la entrada y que avise si no es un entero lo que se ha introducido. Quizá ahora el problema no parezca tan sencillo. No obs tante, no debe apurarse demasiado: le tenemos reservado un excelente co mienzo. Ya tenemos nombre para nuestra nueva función: se llamará getint( ). Planteamiento
Afortunadamente, también tenemos una estrategia pensada. En primer lugar, hay que advertir que cualquier entrada puede leerse como tira de ca racteres. Por ejemplo, el entero 324 se puede interpretar como una tira de tres caracteres: el carácter “3”, el carácter “2” y el carácter “4”. Esto sugie re el siguiente planteamiento: 1. Leer la entrada como tira de caracteres. 2. Comprobar que esta tira se compone exclusivamente de caracteres nu méricos precedidos, quizá, de un signo más o menos. 3. Si es así, convertir la tira en el valor numérico correcto. 4. Si no, enviar un aviso. Este planteamiento es tan genial que tiene que funcionar con toda seguri dad (el hecho accesorio de que ha venido utilizándose durante años nos da una confianza adicional). Sin embargo, antes de pasar este seudocódigo a lenguaje de ordenador deberemos pensar un poco más sobre lo que nuestra función debe hacer. En concreto, antes de empezar a gastar neuronas en las interioridades de getint( ) tenemos que decidir exactamente cómo debe interaccionar la fun ción con su entorno; en otras palabras, cuál va a ser el flujo de información. ¿Qué información deberemos enviar desde el programa de llamada? ¿Qué información deberá devolverte como retorno? ¿En qué forma se deberá ex presar esa información? Una vez más intentamos imaginar nuestra función como caja negra. Nuestro primer objetivo es decidir lo que va y lo que vuel ve; después nos preocuparemos por lo que hay dentro. Este método consigue una interacción mucho más suave entre las diferentes partes de un progra ma. De otro modo, se podría acabar encontrando la forma de instalar el mo tor de una locomotora en un fórmula 1. La función general sería correcta, el problema sería la adaptación en sí. Flujo de información para getint( )
¿Qué tipo de salida deseamos para nuestra función? Evidentemente, de be entregar el valor del número que lee. Por supuesto, scanf( ) también lo hace. En segundo lugar —y esta es la razón por la que estamos gastando tiem po en crear esta función—, debe ser capaz de distinguir lo que lee, es decir, si lo que ha encontrado es o no un entero. Si deseamos que la función sea
realmente útil, deberá ser capaz también de indicar cuándo encuentra un rácter EOF. Así se podría utilizar getint( ) en un bucle while que pudiese es tar leyendo enteros hasta encontrar un carácter EOF. En resumen, deseamos que getint( ) devuelva dos valores: el entero y una información sobre lo leí do. Así pues, necesitamos que la función devuelva dos parámetros, por lo que queda excluido del empleo de return. Se podrían usar dos punteros; sin em bargo, la solución más común para este tipo de problemas es utilizar punte ros para hacer la mayor parte del trabajo de la función, y usar a continua ción return para enviar algún tipo de código al programa de llamada. De he cho, scanf( ) hace justamente esta operación. Devuelve el número de ítems que ha encontrado, y también el carárter EOF si lo detecta. No hemos utili zado esta característica de scanf( ) hasta ahora, pero podemos hacerlo em pleando una llamada a la función de este tipo: quees = scanf("%d", &numero) ;
Siguiendo este modelo, nuestra llamada a la función tendrá el siguiente aspecto: quees = getint(&numero);
La parte derecha utiliza la dirección de número para asignar un valor a número, mientras que el valor de quees se asignará por medio del return. ENTRADA
SALIDA
Dirección de la variable "int”
valor de la variable "int"
return (información adicional)
Figura 10.2
Diseño de la función getint( )
También debemos decidir un código para la información contenida en quees. Como sabemos, una función no declarada se supone de tipo int, por lo que nuestro código lo formaremos con enteros. Vamos a utilizar la siguiente convención: — 1 significará que se ha encontrado un carácter EOF. 1 significará una tira de caracteres que contiene, por lo menos, un carácter no dígito. 0 significará una tira compuesta únicamente por dígitos. 293
En resumen, nuestra función getint( ) tiene una entrada, la dirección de la variable entera cuyo valor va a leerse. Tiene también dos salidas: primera, el valor del entero leído, que se obtendrá por medio de un puntero (el argu mento del puntero, por tanto, funciona como un canal de información de doble vía); segunda, un código que se enviará por medio de return. Con es tos detalles, el esqueleto de nuestra función será: getint(ptint) int *ptint;
/* puntero a entero */
{ int quees;
}
return(quees);
¡Grandioso! Lo único que nos falta es rellenar el interior de nuestra función. El interior de getint( )
El planteamiento general expresado hasta ahora se puede resumir en seudocódigo de la siguiente forma: leer la entrada como caracteres while no se encuentra un EOF, colocar los caracteres en una tira if se encuentra EOF, enviar código STOP else Comprobar la tira, convertir en entero si es posible y enviar el código de control (SINUM o NONUM) Vamos a utilizar STOP, SINUM y NONUM como constantes simbóli cas, que representen —1, 0 y 1, respectivamente, de la forma descrita con an terioridad. Todavía tenemos que tomar algunas decisiones que conciernen al diseño. ¿Cómo decidirá la función que se ha terminado la entrada de una tira? ¿De bemos limitar la longitud de la tira? Nos introducimos en un territorio en el que tenemos que decidir entre la conveniencia del programador y la conveniencia del usuario. La forma más conveniente de atacar el problema sería terminar la tira con una tecla [enter]. Así se conseguiría tomar una sola entrada por línea. Por otra parte, sería muy agradable al usuario poder colocar varios números en la misma línea: 2 34 4542 2 98 Por esta vez nos inclinaremos por el bando del usuario. La función consi derará una tira como un conjunto de caracteres que comienza por un carác ter no blanco ni nuevalínea, y finaliza cuando se encuentre el próximo carác ter blanco o nuevalínea. De esta forma, la entrada se podrá hacer en una sola línea o en varias, a elección.
Limitaremos la longitud de la tira de entrada a 80 caracteres. Como es sabido, las tiras finalizan con un carácter nulo, por lo que necesitaremos un array de 81 caracteres para dar cabida a este último. No deja de ser extraor dinariamente generosa nuestra manera de organizar la entrada, ya que se ne cesitan únicamente 6 caracteres para introducir un entero de 16 bits con sig no. Evidentemente, se podrán introducir enteros más largos, pero serán cor tados hasta el tamaño permitido. Con el fin de hacer el programa más modular, encomendaremos la con versión real de la tira en número a otra función, que llamaremos stoi( ). Tam bién adoptaremos el return de stoi( ) para que envíe un código con informa ción pertinente a getint( ), de manera que esta última pueda enviar su propio informe al programa de llamada. La función stoi( ) ejecutará las dos últimas líneas del seudocódigo indicado arriba. En la figura 10.3 se presenta el programa que hemos preparado para getint( ). No está incluido stoi( ), que se introducirá más adelante. /* getint() */ #include #define LEN 81 /* longitud maxima de la tira de numeros */ #define STOP -1 /* codigos de error para quees */ #define NONUM 1 #define SINUM O getint(ptint) int *ptint; /* puntero al entero de salida */
{ char intarr[LEN]; int ch; int ind = O;
/* para almacenar tira de entrada */ /* indice del array */
while ( (ch = getchar()) == '\n' || ch == ' ' || ch == '\t'); /* salta caracteres nueva linea, blancos y tabulados */ while (ch != EOF && ch != '\n' && ch != ' ' && ind < LEN)
{ intarr[ind++] = ch; /* introduce caracter en array */ ch = getchar(); /* toma otro caracter */ }
intarr[ind] = '\0'; /* acaba array con un caracter nulo */ if (ch == EOF) return(STOP); else return (stoi(intarr, ptint)); /* hace la conversion */
} Figura 10.3 Programa para getint()
Comenzamos con un carácter ch. Si es blanco, o nueva línea, o tabulado, obtenemos el siguiente carácter hasta que hay uno que no lo es. Si este últi mo tampoco es un EOF, lo colocamos en un array. Los caracteres que ven gan a continuación se siguen introduciendo en dicho array hasta encontrar un carácter prohibido o alcanzar el tamaño límite. Colocamos a continua295
ción un carácter nulo (‘ \ 0’) en la siguiente posición del array, con el fin de marcar el final de la tira de caracteres. Con ello conseguimos transformar el array en una tira de caracteres estándar. Si el último carácter leído es un EOF, devolvemos STOP por medio del return; en caso contrario, seguimos adelante e intentamos transformar la tira en enteros. Para ello llamamos a la función stoi( ) que tiene encomendado ese trabajo. ¿Qué hace stoi( )? To ma como entrada una tira de caracteres y un puntero a una variable entera. Deberá utilizar el puntero para asignar un valor a la propia variable; tam bién utilizará return para enviar un informe sobre los problemas encontra dos en la lectura de la tira, informe que será, a su vez, utilizado por getint( ) para elaborar el suyo propio. ¡Estamos jugando con dos barajas! Una forma menos compacta de representar la utilización de stoi( ) es: quees = stoi(intarr, ptint); return (quees);
numérico. Supongamos que el carácter es ‘4’. Este carácter tiene un valor numérico ASCII 52, que es la forma en que se ha almacenado. Si restamos 48, obtenemos 4, es decir, ‘4’ - 48 = 4 pero 48 es simplemente el código ASCII del carácter ‘0’; por tanto, ‘4’ - ‘0’ = 4 De hecho, esta última sentencia se cumplirá para cualquier código que utilice números consecutivos para representar dígitos consecutivos. Si llamamos num al valor numérico y chn es un carácter que representa un número, tendre mos: num = chn - ’0’;
donde quees sería una variable de tipo int. La primera sentencia asignaría un valor a lo que ptint estuviese apuntando, independientemente de lo que sea, y también asignaría ese valor a quees. La segunda sentencia devolvería el valor al programa que había llamado previamente a getint( ). La única lí nea de programa incluida en la figura anterior tiene exactamente el mismo efecto, con la diferencia de que no se ha empleado una variable intermedia. Bien, lo único que falta es escribir stoi( ), y habremos acabado el tra bajo.
Con esta técnica podemos convertir el primer carácter en un número. A continuación buscaremos el siguiente miembro del array. Si es ' \ 0 ' había un único número, y ya hemos terminado. Supongamos, sin embargo, que sea un ‘3’. Lo convertiremos en el valor numérico 3; pero si hay un 3 en esa posición, nuestro 4 debía ser en realidad 40, y el total es 43:
Conversión de tira de caracteres en entero: stoi( )
Lo único que tenemos que hacer ahora es continuar este proceso indefini damente multiplicando el antiguo valor de num por 10 cada vez que encon tremos un nuevo dígito. En nuestra función se emplea esta técnica. A continuación presentamos el listado de stoi( ). Lo mantendremos en el mismo fichero que getint( ), por lo que usaremos los mismos #define.
Describiremos, en primer lugar, la entrada y salida que debe tener esta función. La entrada habrá de ser una tira de caracteres, por lo que stoi( ) deberá tener un argumento de tipo tira. También habrá dos valores de sali da: la conversión a entero y el informe antes mencionado. Utilizaremos re turn para este informe, pero tendremos que emplear un puntero para devol ver el otro valor. Así pues, deberá haber un segundo argumento, que será un puntero a entero. El esqueleto de nuestra función tendrá un aspecto como éste:
num = 10 * num + chn - ’0’;
/* convierte una tira en entero y comprueba el resultado */ stoi(string, intptr) char string[]; /* tira a ser convertida en entero */ int *intptr; /* valor del entero */
{
int signo =1; /* tiene en cuenta el signo + o - */ int indice = 0;
stoi(strinq, intptr) char string[]; /* string de entrada */ int *intptr; /* puntero a la variable valor del entero */
if (string[indice] == '-' ||
{
}
string[indice] == '+' )
signo = (string[indice]== '-') ? -1 : 1; /* pone signo */
int quees;
*intptr = 0; /* inicializa valor */ while (string[indice] >= '0' && string[indice] <= '9') *i nt pt r = 10 * (*i nt pt r) + st ri ng[ i ndi ce++] - ' 0' ; i f ( st ri ng[ i ndi ce] == ' \ 0 ' ) {
return(quees) ;
*intptr = signo * (*intptr); return(SINUM) ;
Bueno, ahora debemos buscar un algoritmo que haga la conversión. Por el momento, ignoraremos el signo y supondremos que la tira contiene única mente dígitos. Miremos el primer carácter y convirtámoslo en su equivalente
}
}
else /* hay un caracter no numerico distinto de '\0' */ return(NONUM);
297
Como se puede observar, funciona satisfactoriamente. Observe que po demos preparar un bucle que lea enteros indefinidamente hasta encontrar un carácter EOF. Esta característica puede resultar de utilidad. ¿Hay algún error? Al menos hay uno. Si colocamos un carácter EOF in mediatamente detrás de un número sin dejar por medio un blanco o un ca rácter nuevalínea, se detiene la frase de entrada y se ignora el último núme ro:
La setencia while sigue funcionando tranquilamente, convirtiendo dígi tos en números, hasta que encuentra un carácter que no corresponda a una cifra. Si tal carácter es un ‘\0’, es señal de que todo ha ido bien, porque es la marca de final de la string. Cualquier otro carácter que no sea un dígito envía el programa al else, que emitirá un informe negativo de la lectura. En la librería C estándar existe una función llamada atoi( ) (conversión ASCII a entero) muy semejante a stoi( ). Las diferencias fundamentales son que stoi( ) comprueba si la tira enviada contiene algún carácter no numéri co, y que atoi( ) utiliza return para devolver el número en lugar de un punte ro; además, atoi( ) hace por su cuenta el salto de blancos que nosotros hace mos en getint( ). En realidad, podríamos haber realizado la comprobación de la tira en getint( ) y haber empleado atoi( ) en lugar de stoi( ), pero pen samos que es más divertido desarrollar nuestra propia función.
Ahora que tenemos una práctica función de capturar enteros, nos pro pondremos un nuevo objetivo que haga uso de la misma.
¿Por qué no las probamos?
Una de las pruebas más comunes a que se somete un ordenador es la or-
¿Es realmente lógica la lógica que hemos usado? La única manera de com probarlo es hacer un test de nuestras funciones en un programa de prueba: / * prueba de getint() */ #define STOP -1 # define NONUM 1 # define SINUM 0 main() { int num, quees;
printf("Este programa deja de leer numeros con EOF.\n"); while((quees = getint(&num)) != STOP) if (quees == SINUM) printf("El numero %d ha sido aceptado. \n", num); else printf("Eso no es un numero!! Pruebe otra vez.\n"); printf("Eso es todo.\n");
706 EOF /* se acepta 706 */ 706EOF /*no se acepta 706 */
.
Ordenación de números
denación de números. Desarrollaremos aquí un programa que ordene ente ros. Como siempre, lo consideraremos como una caja negra de la que sólo nos preocupará su entrada y salida. Nuestro plan general, mostrado en la fi gura 10.4, es agradablemente sencillo.
números ordenados
números
}
Una posible salida sería:
Figura 10.4
Programa de ordenación como caja negra Este programa deja de leer numeros con EOF. 100 -23 El numero 100 ha sido aceptado. El numero -23 ha sido aceptado. +892
El numero 892 ha sido aceptado. flock
Eso no es un numero!! Pruebe otra vez.
Por el momento, el programa está aún demasiado nebuloso para poderlo pasar a código C. El siguiente paso es identificar las principales tareas que el programa debe realizar para alcanzar la meta propuesta. Se puede dividir este programa en tres tareas principales:
23tururu
Eso no es un numero!! Pruebe otra vez. 775 El numero 775 ha sido aceptado. [control-z] (envia el caracter EOF en nuestro sistema) Eso es todo.
1. leer los números, 2. ordenarlos, 3. imprimir los números ordenados. 299
La siguiente figura muestra esta subdivisión, conforme nos movemos del nivel mayor de organización a un nivel más detallado.
números ordenados
números
Figura 10.5
Programa de ordenación: curioseando en su interior.
Hasta ahora hemos logrado transformar nuestra caja negra en tres cajas más pequeñas, cada una con su propia entrada y salida. Podríamos asignar cada parte a un equipo diferente de programación, con la única salvedad de que deberíamos asegurarnos que la salida numérica de “leer” tuviese el mis mo formato que el empleado por “ordenar” como entrada. Como verá, insistimos una vez más en la modularidad. Hemos dividido el problema original en tres problemas menores y, por ende, más maneja bles. ¿Qué viene ahora? Dedicaremos nuestros esfuerzos a cada una de las tres cajas separadamente, dividiéndolas en unidades cada vez más simples, hasta alcanzar un punto en que la codificación en lenguaje C sea obvia. Mientras estamos en ello, dedicaremos nuestra atención a tres detalles de importancia: la elección del formato de los datos, el filtrado de errores y el flujo de infor mación. Para continuar con nuestro ejemplo, comencemos con la sección de dicada a lectura. Lectura de datos numéricos
Un gran número de programas requiere entradas numéricas, de forma que las ideas que desarrollemos aquí podrán ser útiles en muchas otras ocasio nes. La forma general de entrada en esta parte del programa está muy clara: usar un bucle para leer números hasta que se hayan leído todos. Sin embar go, no es tan fácil como parece.
números
números
Elección de la representación de datos
¿Cómo se puede representar un paquete de números? Podemos usar un paquete de variables, uno por número. Esta solución es tan problemática que más vale olvidarla. También podemos emplear un array, con un elemento por número. Esto suena mucho mejor, por lo que usaremos un array. Pero, ¿qué tipo de array ¿De tipo int? ¿De tipo double? Necesitamos sa ber para qué se va a usar el programa. Supondremos que se va a usar con enteros. (¿Y qué sucedería si fueran números de tipos diferentes? Es posible hacerlo, pero rebasa, por ahora, nuestras miras.) Usaremos un array de en teros para almacenar los números que vayamos leyendo. Final de la entrada
¿Cómo sabe el programa la cantidad total de números que hay que leer? En el capítulo 8 hemos discutido varias soluciones a este problema, la mayo ría poco satisfactorias. Pero ahora disponemos de getint( ), por lo que nues tro problema no es problema. Una posible solución sería: leer un número while not EOF asignarlo a un array y leer el siguiente número si el array no está completo Observe que hay dos condiciones separadas que hacen que finalice la en trada de números: una señala EOF o un array completo. Otros aspectos
Antes de pasar estas ideas a código C tenemos que tomar aún algunas decisiones. ¿Qué hay de comprobación de errores? ¿Debemos hacer esta parte del programa como función aparte? Con la primera pregunta queremos significar si vamos a tomar precau ciones contra la posibilidad de que el usuario introduzca datos erróneos, co mo letras en lugar de números. Sin getint( ) deberíamos apoyarnos en la “teo ría del perfecto usuario”, que establece que el usuario, por definición, no comete errores. Sin embargo, todos sabemos que esta teoría funciona con nosotros mismos, pero no con los demás usuarios. Por fortuna, disponemos del informe enviado por getint( ) para ayudarnos en estos casos. La programación que falta se puede adaptar fácilmente a main( ); sin em bargo, es más modular utilizar una función distinta para cada una de las tres partes principales del programa, y así es como lo haremos. La entrada a esta función serán números desde teclado o desde un fichero, y la salida será un array que contenga los números desordenados. Sería interesante que el pro grama principal supiese cuántos números se han leído; por tanto, haremos que nuestra función envíe también esta cantidad como salida. Por último, intentaremos ser un poco amistosos con el usuario, por lo que enviaremos 301
Veamos qué aspecto tiene getarray( ): un mensaje indicando los límites a los que se ha de sujetar, así como un “eco” de su entrada. main( ) y getarray( )
Llamaremos a nuestra función de lectura getarray( ). Ya hemos definido la función en términos de entrada y salida, y hemos esquematizado su inte rior en seudocódigo. Escribamos ahora la función y ocupémonos a continua ción de adaptarla en el programa principal. El programa main( ) sería:
/* getarray() usando getint() */
#define STOP -1 /* detecta EOF */ #define NONUM 1 /* detecta entrada no numerica */ #define SINUM 0 /* acepta numeros */ getarray(array, limite) int array[], limite; { int num, quees; int indice = 0; /* indice del array */ printf("Este programa deja de leer a los %d numeros\n", limite); printf("o si se pulsa un EOF.\n"); while(indice < limite && (quees = getint(&num)) != STOP) {
/* ordena1 * / #define TAMMAX 100 /* limite de numeros a clasificar */ main () { int numeros[TAMMAX] ; /* array para entrada */ int total; /* numero de entradas * /
}
/* deja de leer al limite o al pulsar EOF */ if (quees == SINUM) { array[index++] = num; printf("El numero %d ha sido aceptado. \n", num);
}
else if (quees == NONUM) printf("Eso no es un entero!! Pruebe otra vez.\n"); else printf("Esto no deberia suceder. Algo va muy mal.\n");
total = getarray(numeros,TAMMAX); / * mete entrada en array*/ ordena(numeros, total); /* ordena el array */ imprime(numeros, total); /* imprime el array ordenado */
}
if (indice == limite) /* avisa si se ha llenado el array * / printf("Tengo completos los %d elementos del array.\n”,
Ya tenemos aquí una perspectiva global del programa. La función getarray( ) coloca la entrada en el array números y, además, devuelve el número total de valores que se han leído; ese número se asigna a la variable total. Vienen a continuación ordena( ) e imprime( ), las cuales tienen aún que es cribir, ordenar el array e imprimir los resultados. Si les enviamos el número total, facilitaremos su tarea y ahorraremos que tengan que hacer sus propias cuentas. Además, definiremos en getarray( ) un tamaño máximo, TAMMAX, que definirá el límite máximo de almacenamiento del array. Ahora que hemos añadido total al flujo de información, deberemos mo dificar, en consecuencia, nuestro diagrama de caja negra. Véase la figura 10.6.
números ordenados
números
F igu ra 10.6
Programa de ordenación: detalles
limite);
}
return(indice);
Es una considerable porción de programa, que conviene aclarar en algu nos puntos. Explicación
Resulta un poco difícil recordar el significado de un código — 1, por ejem plo, por lo que hemos empleado constantes simbólicas mnemotécnicas para representar los códigos de error. Hemos preparado getarray( ) para manejar cada uno de los posibles có digos. Un código STOP produce una parada en el ciclo de lectura cuando getint( ) se encuentra un EOF cerrándole el paso. Un código SINUM produ ce el almacenamiento del número en el array que estaba esperándolo; ade más, el número envía un “eco” al usuario para comunicarle que ha sido acep tado. Por su parte, un código NONUM envía al usuario un mensaje para que lo intente de nuevo. (A esto se llama relaciones de buena vecindad.) Aún hay otra sentencia else. En buena lógica, la única forma de alcanzar esta sentencia es que getint( ) devuelva un valor distinto de -1, 0 ó 1. Sabe mos, sin embargo, que estos valores son los únicos que van a devolverse, por lo que parece que nuestra última sentencia es inútil. ¿Por qué la incluimos? La utilizamos como ejemplo de “programación defensiva”, que es el arte 303
de proteger un programa contra futuras manipulaciones. Algún día, noso tros, o algún otro, podemos intentar manipular getint( ) añadiendo algunos otros códigos de nuestro propio repertorio. Probablemente habremos olvi dado, o nunca habrán sabido, que getarray( ) supone una de entre tres úni cas posibles respuestas. La función de este else es atrapar cualquier nueva respuesta que aparezca, lo que permitirá al eventual manipulador detectar rápidamente el error cometido y obrar en consecuencia. El tamaño del array queda establecido en main( ). Así pues, no tenemos que incluir este tamaño cuando declaramos el argumento del array en getarray( ). Sin embargo, lo hacemos incluyendo los corchetes, con el fin de de jar claro que el argumento es un array.
bemos tener en cuenta que los arrays comienzan sus índices por 0, no por 1. Observe el código presentado y trate de comprobar si funciona. La mane ra más fácil de hacerlo es imaginar que límite vale 1, y seguir el programa sentencia a sentencia. Frecuentemente, la parte más complicada de un programa es la consecu ción de una interacción en forma conveniente con el usuario. Es el caso de nuestro programa. Ahora que hemos conseguido preparar getarray( ), ob servaremos que ordena( ) es bastante más sencilla, e imprime( ), más senci lla aún. Vayamos con ordena( ) en primer lugar. Ordenación de los datos
Observemos de nuevo main( ): int numeros[TAMMAX] ; /* define tamano en main */ int array[]; /* no se indica tamano en la funcion llamada
*/
números
Discutiremos más profundamente el empleo de los arrays en funciones en el capítulo 12. También hemos decidido usar la palabra clave return para comunicar al programa de llamada el número de valores leídos. Nuestra llamada de fun ción, por tanto, será total = getarray(numeros);
números ordenados
total
main()
{ int numeros[TAMMAX]; /* array para entrada int total; / * numero de entradas * /
asignando un valor a total y dando valores al array número. Se estará preguntando por qué no hemos usado punteros en la llamada
*/
total = getarray(numeros,TAMMAX); /* mete entrada en array ordena(numeros, total); / * ordena el array * / imprime(numeros, total); /* imprime el array ordenado */
total = getarray(&numeros);
} Después de todo, necesitamos que la función cambie el valor de algo (del array) del programa de llamada. La respuesta quizá le sorprenda: ¡Sí, esta mos usando un puntero! En C, el nombre de un array es también un puntero al primer elemento del mismo, es decir: numeros == &numeros[0]
Cuando getarray( ) prepara el array array, la dirección de array[0] es la misma que la dirección de números[0], y lo mismo sucede con el resto de los subíndices. Así, todas las manipulaciones que getarray( ) haga en array[ ] se están haciendo, en realidad, en números[ ]. También hablaremos de la re lación entre punteros y arrays en el capítulo 12. El hecho más notable que necesitamos saber por el momento es que empleamos un array como argu mento de función, afectando la función al array del programa de llamada. Cuando una función utiliza contadores y límites, como es nuestro caso, la mayoría de los errores aparecen en las “condiciones de contorno”, allá donde el contador alcanza su límite. ¿Estamos leyendo un máximo de TAMMAX números, o los estamos pasando en uno? Tenemos que prestar aten ción a detalles como + + índice y/o índice + + , y < y/o < = . También de
Como vemos, la entrada a ordena( ) es un array de enteros a ser ordena dos y un contador con el número de elementos que se incluyen. La salida es el propio array con los números ordenados. Aún no hemos decidido cómo ordenarlos, por lo que tendremos que afinar la descripción posteriormente. Una cuestión obvia que hay que decidir es la dirección de ordenación. ¿Va mos a ordenar los números de mayor a menor, o viceversa? De nuevo, hay que ser arbitrarios en este punto, y decidiremos ordenar de mayor a menor. (Podríamos hacer un programa que permitiese elegir entre ambas opciones, pero tendríamos también que desarrollar la forma en que el programa deci diera cuál de las dos opciones tomar.) Nos ocuparemos ahora del método de ordenación. Hay un gran número de algoritmos de ordenación para ordenadores; aquí usaremos uno de los más sencillos. La estrategia a seguir, expresada en seudocódigo, es: for n = primero a n = penúltimo elemento encontrar número ma yor y colocarlo en la posición del elemento n. 305
*/
La filosofía es la siguiente: la primera vez, n = 1. Buscamos en todo el
array, encontramos el número mayor, y lo colocamos como primer elemen to. A continuación, n = 2, y repetimos la operación en todos los números del array excepto en el primero, encontrando el mayor número entre los res tantes y colocándolo en segunda posición. Continuamos este proceso hasta que alcanzamos el penúltimo elemento. En ese momento, quedan sólo dos elementos por colocar. Los compara mos y colocamos el mayor en penúltima posición. Así se queda confinado el menor elemento en la posición final. Esta tarea parece que ni pintada para un bucle for, pero falta por descri bir el proceso de “encontrar y colocar el número” con mayor detalle. ¿Có mo se puede encontrar el número mayor cada vez? Hay un forma muy senci lla: compárense el primero y segundo elemento de la porción de array de la que tengamos que extraer el número mayor; si el segundo es mayor, intercámbiense los dos valores; compárese ahora el primer elemento con el terce ro; si el tercero es mayor, intercámbiense también; cada vez que se realiza esta operación, el elemento mayor “flota” hasta la parte superior; continuamos este proceso hasta comparar el primer elemento con el último; cuando haya mos terminado, el número mayor estará colocado como primer elemento de su porción de array. En resumen, hemos ordenado el primer elemento del array, pero el resto es aún una mezcolanza. Expresado en seudocódigo:
Hemos llamado tope al índice del elemento del array que ha de ser ocupa do, ya que está en el extremo de la parte no ordenada del array. El índice busca, se pasea por la parte de array situada por debajo del elemento tope. La mayor parte de los libros emplean i y j como índices, pero así es más difí cil observar lo que está sucediendo. El algoritmo recibe el nombre de “ordenación por burbuja”, porque los valores mayores van flotando lentamente hacia la “superficie”. Por último, nos queda por escribir la función imprime( ). Impresión de los datos números ordenados imprimir
números ordenados
total
Esta función es la más sencilla: /* imprime un array */ imprime(array, limite) int array[], limite;
{
int indice;
for n = segundo elemento hasta último elemento comparar elemen to n con primer elemento; si n es mayor, intercambiar valores.
for (indice = O; indice <= limite; indice++) printf("%d\n", array[indice]);
} De nuevo la tarea tiene aspecto de bucle for. Podrá estar anidada dentro del primer bucle; el bucle externo indicará qué elemento del array hay que rellenar, y el interno buscará el valor a colocar allí. Si ponemos las dos partes de seudocódigo juntas y las traducimos a C, tenemos la siguiente función: ordena el array en orden decreciente */ ordena(array, limite) int array[], limite; /*
{
int tope, busca;
}
for (tope = 0; tope < limite-1; tope++) for (busca = tope + 1; busca < limite; busca++) if (array[busca] > array[tope]) intercambia(&array[busca], &array[tope]);
Aquí hemos conseguido acordarnos de que el primer elemento tiene su bíndice 0. También nos hemos acordado de que desarrollamos una función de intercambio en el capítulo 9, por lo que la vamos a emplear aquí. Como intercambia( ) funciona sobre dos elementos del array, y no sobre el array completo, hemos usado las direcciones de los elementos correspondientes. (El nombre array es un puntero al array completo, pero se debe usar el operador & para apuntar a un solo miembro.)
Si deseamos algo diferente, como imprimir en filas en lugar de una sola columna, siempre podemos volver atrás y cambiar esta función sin modifi car las demás. De igual forma, si encontrásemos un algoritmo de ordenación que nos gustase más, podríamos reemplazar aquel módulo. Esta es una de las grandes ventajas que tiene la programación modular. Resultados
Compilemos y comprobemos el paquete obtenido. Para estudiar con fa cilidad las condiciones de contorno, haremos TAMMAX momentáneamente igual a 5. En nuestra primera ejecución, introduciremos números en el programa hasta que rehúse aceptar más. E ste p rogram a deja de lee r a lo s 5 nu m ero s o si se pulsa un E O F .
12 34 54 23 67 T e n g o c o m p le t o s l o s 5 e l e m e n t o s d e l a r r a y .
67 54 34
23
12
307
Bien, se ha detenido al leer 5 números y ha ordenado los resultados. Com probaremos ahora que es capaz de detenerse por un carácter EOF. Este programa deja de leer a los 5 numeros o si se pulsa un EOF. 456 928 -23 +16 [control-z]
(transmite EOF en nuestro sistema)
928 456 16
En menos tiempo del que se tarda en decir “el coturno alácrelo de la efí mera existencia vivencial”, el enorme array introducido ha sido ordenado. ¡Exito total! No era fácil, pero tampoco imposible. Dividiendo el proble ma en parte menores, y deteniéndonos cada vez en el flujo de información que se debe ir acarreando, hemos reducido el problema a proporciones ma nejables. Además los módulos individuales podrán utilizarse como parte de programas similares. Con esto finalizamos los ejemplos del capítulo. Revisaremos ahora bre vemente los distintos conceptos que han aparecido en el mismo.
Resumen ¿Qué hemos conseguido? En el lado práctico, hemos desarrollado un generador de números aleatorios y un programa de ordenación de enteros; durante el proceso, hemos preparado una función getint( ), que puede utili zarse con otros programas; desde el punto de vista educativo, hemos ejem plarizado algunos principios generales y conceptos útiles en el diseño de programas. La consecuencia más importante que se debe sacar es que los programas deben ser diseñados, en lugar de lucubrados en un proceso aleatorio de cre cimiento, ensayo y error. Antes de escribir una sola línea de programa, debe rá pensar cuidadosamente el formato y contenido de su entrada y salida; in tentará subdividir el programa en tareas bien definidas, programar las tareas por separado y no descuidar la interrelación entre las mismas. La idea a per seguir es llegar a la máxima modularidad. Donde sea necesario, divida un módulo en módulos menores. Utilice funciones para aumentar la modulari dad y claridad del programa. Cuando diseñe un programa, intente anticiparse a los posibles errores, piense en las distintas alternativas que pueden ir mal y programe de acuerdo con ellas. Utilice trampas para errores, protegiendo problemas potenciales, o, al me nos, envíe un mensaje de alerta al usuario cuando aparezca un problema. Es mucho mejor dar al usuario una segunda oportunidad para introducir un da to que dejar que el programa muera ignominiosamente. Cuando diseñe una función, decida en primer lugar su interacción con el programa de llamada. Establezca el flujo de información de entrada y de salida. ¿Cuáles serán los argumentos? ¿Emplearemos punteros, return o am bos? Una vez decididos los parámetros de diseño, se podrá dedicar a la parte mecánica de la función. Si observa estos principios generales, sus programas serán más prácticos y menos proclives a errores. Conseguirá tener una colección de funciones pa ra uso general. Le llevará menos tiempo programar una determinada aplica ción. Por encima de todo, las ideas apuntadas son una excelente receta para conseguir una programación saludable. No olvide los modos de almacenamiento. Las variables se pueden definir dentro o fuera de las funciones; en este último caso serán variables externas (o globales), y podrán ser utilizadas en más de una función. Las variables definidas dentro de la función son locales a dicha función y desconocidas pa ra las demás. Como norma, utilice la variedad automática en variables loca les siempre que sea posible. Así conseguirá que las variables de la función no se vean contaminadas por las actuaciones de las demás.
309
guiente número más alto. Toda la información que se obtuvo durante la primera búsqueda queda olvidada, con excepción del número máximo; el segundo número más alto podría haber estado colocado en la posición 1 durante un tiempo, y después sería barajado con los demás hasta caer en el fondo. Así, debemos repetir un gran número de comparaciones realizadas la primera vez, durante el segundo ciclo, el tercero, etc.
Hasta ahora hemos aprendido Cómo imaginar una función: una caja negra con flujo de información. Qué es la “comprobación de errores” y por qué es conveniente. Un algoritmo de ordenación. Cómo hacer que una función cambie un array. función (array) Cómo convertir una tira de dígitos en un número. Los modos de almacenamiento: auto, extern, static y register. El alcance de cada modo de almacenamiento. Qué modo de almacenamiento emplear: casi siempre, auto.
2.
Sustituya array[busca] > array[tope] por
array[busca] < array[tope] 3. /* imprime un array * / imprime(array, limite) int array[], limite; int indice; for (indice = 0; indice <= limite; indice++)
{
printf("%10d ", array[indice]); if (indice %5 == 4) printf(”\n");
Cuestiones y respuestas
}
Cuestiones
1. Indique una situación que demuestre una cierta ineficiencia en nuestro algoritmo de ordenación. 2. ¿Qué cambios habría que introducir en la subrutina de ordenación para hacer que se ordenase de menor a mayor? 3. Modifique imprime( ) para que la salida contenga cinco números por línea. 4. ¿Cómo habría de alterarse stoi( ) para manejar tiras que representasen números octales? 5. Indicar qué funciones conocen cada una de las variables en el siguiente progra ma. ¿Hay algún error? fichero 1 */ int margarita; main() /*
{ int lirio;
}
petalo()
{
extern int margarita, lirio;
}
/* fichero 2 */ static int lirio; int rosa; tallo()
{
int rosa;
}
}
printf("\n”);
4. En primer lugar, limite los caracteres aceptables a los dígitos 0 a 7. Más tarde, multiplique
por 8 en lugar de por 10 cada vez que se detecte una nueva cifra. 5. Main( ) conoce a margarita por defecto; también la conocen pétalo( ) y raíz( ) debido a la declaración extern. Sin embargo, tallo( ) no la conoce porque están en ficheros diferen tes. El primer lirio es local en main; la referencia a lirio realizada en pétalo( ) es un error, por que no hay ningún lirio externo en los ficheros. Existe un lirio estático externo, pero lo conocen únicamente las funciones del segundo fi chero.
Por otra parte, raíz( ) conoce a la variable rosa externa, pero tallo( ) la sustituye por su propia rosa local.
Ejercicios 1. Algunos usuarios pueden verse perdidos por no saber introducir un carácter EOF. a. Modifique getarray( ) y sus funciones de llamada de forma que puedan utili zar un carácter # en su lugar. b. Haga otra modificación que permita utilizar EOF o # alternativamente. 2. Escriba un programa que ordene números float. 3. Prepare un programa que convierta un texto de letra mayúscula y minúscula en mayúscula únicamente. 4. Escriba un programa que produzca texto a doble espacio a partir de texto a espa cio sencillo.
raiz() { extern int margarita;
}
Respuestas
1. Supongamos que estamos ordenando 20 números. El método realiza 19 comparaciones para encontrar el número mayor de todos; a continuación realiza 18 para encontrar el si
311
11 El preprocesador C En este capítulo encontrará: • • • • • • • •
Constantes simbólicas: #define Utilización de argumentos con #define ¿Macros o funciones? Inclusión de un fichero: #include • Ficheros de encabezamiento: ejemplo Otros comandos: #undef, #if, #ifdef, #ifndef, #else y #endif Hasta ahora hemos aprendido Cuestiones y respuestas Ejercicios
313
El preprocesador C CONCEPTOS Comandos del preprocesador Constantes simbólicas Macros y “macrofunciones” Efectos colaterales de las macros Inclusión de ficheros Compilación condicional
COMANDOS DEL PREPROCESADOR #define, #include, #undef, #if, #ifdef, #ifndef, #else, #endif
Constantes simbólicas: #define El comando #define, como todos los del preprocesador, empieza con el símbolo #. Puede aparecer en cualquier lugar del fichero fuente, y la defini ción es válida desde el lugar en que aparece el comando hasta el final del fi chero. Como se ha podido ver en los capítulos anteriores, lo hemos usado para definir constantes simbólicas en nuestros programas, pero su acción no se limita sólo a esto, sino que posee nuevas aplicaciones, como veremos a continuación. En el ejemplo siguiente se ilustran algunas de las propiedades y posibilidades del comando #define. / * Ejemplos sencillos de preprocesador */ #define DOS 2 /* se pueden usar comentarios */ #define MSJ "Gato escaldado del agua fria \ huye. " / * una barra-atras continua definicion en otra linea */ #define CUATRO DOS*DOS #define PX printf("X es %d.\n", x) #define FMT "X es %d.\n" main()
{ int x = DOS; PX; x = CUATRO; printf(FMT, x); printf("%s\n", MSJ); printf("DOS: MSJ\n");
}
El lenguaje C se desarrolló con el fin de cubrir las necesidades de los pro gramadores atareados, y a este tipo de personas les gusta disponer de un pre procesador. Cuando escriba un programa en C, no es necesario que lo haga en detalle, sino que parte del trabajo pesado se lo puede dejar a este “cola borador”; él se encargará de leerse su programa antes de que caiga en las manos del compilador (de ahí el nombre de preprocesador), y, siguiendo las indicaciones que le haya dejado a lo largo del programa fuente, sustituirá abreviaturas simbólicas por las direcciones que representan, buscará otros fi cheros donde puede tener escritos trozos de programa e incluso puede to mar decisiones sobre qué partes enviar al compilador y qué partes no. Esta breve descripción no hace justicia a la gran ayuda que representa este inter mediario; quizá se vea más claro por medio de algunos ejemplos. Ya hemos visto numerosos ejemplos de #include y #define a lo largo del libro; lo que haremos será recopilar todos los posibles usos que ya hemos aprendido y añadirles algunos nuevos.
Cada definición consta de tres partes. En primer lugar, el comando #define; seguidamente la palabra que queremos definir, que se suele denominar “macro” dentro del mundillo de los ordenadores; la macro debe ser una sola pa labra y, por tanto, no debe tener ningún espacio en blanco dentro de ella; por último, tenemos una serie de caracteres (llamados “caracteres de recam bio”) que van a ser representados por la macro. Cuando el preprocesador se encuentra a lo largo del programa con una macro definida anteriormente, casi siempre la sustituye por los caracteres de recambio (hay una excepción, como veremos dentro de un momento). AI proceso de cambio, por los carac teres que representa, se le denomina “expansión de la macro”. Obsérvese que el preprocesador hace el mismo caso a los comentarios que el compila dor de C, es decir, ninguno. La mayoría de los sistemas permiten, además, la continuación de una definición en otras líneas por medio del carácter barraatrás (‘ \ ’), no incluyéndolo en los caracteres de recambio, por supuesto. Ejecutemos el ejemplo y veamos cómo funciona. X es X es Gato DOS:
2. 4. escaldado del agua fria huye. MSJ 315
que cuando el programa se ejecute, se almacene en un array cuyo último ele mento sea el carácter nulo. Así # d e fin e
hal
#d e f i n e
Figura 11.1
Elementos de la definición de una macro
He aquí lo que ha ocurrido. La sentencia int x = dos; se convierte en int x = 2;
ya que DOS es sustituido por 2. A continuación, la sentencia PX; se convierte en príntf"X es %d.\n",x);
en una sustitución al por mayor. Este es un nuevo paso adelante; hasta ahora solamente habíamos empleado las macros para definir constantes. Podemos ver aquí que una macro puede simbolizar cualquier serie de caracteres, inclu yendo a toda una expresión C. Observe que, al ser PX una constante, sola mente escribirá el valor de una variable que se llame x. La siguiente línea nos depara otra sorpresa. Podría haber supuesto que CUATRO se sustituiría por 4, pero lo que ocurre realmente es: x=
cuatro ;
se convierte en x = dos *dos ; que pasa a x = 2*2;
y ahí termina el preprocesador su trabajo. La multiplicación no se realiza du rante el preprocesado, ni siquiera durante la compilación, sino cada vez que se ejecute el programa. El preprocesador no sabe multiplicar, se limita a cam biar unos caracteres (la macro) por otros (los caracteres de repuesto) de una forma bastante literal. Como habrá observado, la definición de una macro puede hacerse en fun ción de otras macros definidas anteriormente. (No obstante, algunos compi ladores no permiten esta definición “anidada”.) En la línea siguiente tenemos
'z ' d e fin e u n a c o n s ta n te d e tip o c a rá c te r m ie n tr a s q u e
hap
" Z " d e fin e u n a t ira d e c a ra c te re s: Z \ O
En general, allí donde el preprocesador encuentra una macro la sustituye por los caracteres que representa. Si entre esos caracteres se encuentran otras macros, también son expandidas. El único caso donde el preprocesador no cambia la macro es si está colocada dentro de una tira de caracteres entre comillas. Por tanto, printf("DOS: M S J " );
escribe DOS: MSJ al pie de la letra, en vez de escribir 2: Gato escaldado del agua fria huye.
Si lo que desea realmente es que se escriba esta última línea, entonces puede poner en su programa printf ( "% d: % s\n " , D O S , MSJ);
ya que ahora las macros no se hallan protegidas por las comillas. ¿Cuándo deben emplearse constantes simbólicas? En principio, deberían usarse en lugar de cualquier número. Si el número es una constante de un cálculo, un nombre simbólico dejará más clara su función. Si el número se emplea para definir el tamaño de un array, una macro le permitirá aumentar más fácilmente el tamaño del array (sobre todo, si hay que cambiar de tama ño varios arrays de las mismas dimensiones). Si el número es un valor de un parámetro del sistema, por ejemplo el carácter EOF, un nombre simbólico hará al programa más portátil; si se va a ejecutar en otro sistema solamente hay que cambiar la definición de EOF. La facilidad mnemotécnica de cam bio de parámetros e independencia del sistema hacen que merezca la pena el empleo generoso de constantes simbólicas. ¿Qué? ¿Le parece fácil, eh? Vamos a ser un poco más intrépidos y vea mos las hermanas pobres de las funciones: las macros con argumentos.
printf(FMT, x) ; que se convierte en printf("X es %d.\n”, x)
al ser FMT sustituida por sus caracteres correspondientes. Si alguna secuen cia de caracteres se repite varias veces a lo largo del programa, puede resul tarle cómodo definir una abreviatura y emplearla en lugar de teclear una y otra vez los mismos caracteres. Las comillas de la definición hacen que los caracteres de repuesto sean tratados como una constante de tipo string, o sea,
317
el argumento de una función. El resultado del programa, al ejecutarse, pue de proporcionarle algunas sorpresas; aquí está z es 16. z es 4. CUADRADO(x) es 16. CUADRADO(x+2) es 14. 1OO/CUADRADO(2) es 100. CUADRADO(++x) es 30.
Las dos primeras líneas pueden considerarse “normales”. Observe, sin embargo, que, a pesar de que x está situada entre comillas en la definición de pr, ha sido sustituida por su argumento correspondiente. ¡Absolutamente todos los argumentos que aparecen en la definición son sustituidos! La línea tercera es más interesante: PR(CUADRADO(x));
se convierte en printf("CUADRADO (x) es %d.\n", CUADRADO (x)) ;
/* macros con argumentos */ #define CUADRADO(x) x*x #define PR(x) printf("x es %d.\n", x) main() {
int x = 4; int z ; z = CUADRADO(x) ; PR (z) ; z = CUADRADO(2); PR(z) ;
en la primera fase de la expansión de la macro. A continuación se expande la segunda aparición de CUADRADO(x), que pasa a ser x*x, mientras que la primera permanece como está, al estar protegida en el programa por un par de comillas. Por tanto, la forma definitiva que llegará al procesador es: printf("CUADRADO(x) es %d.\n",
x*x);
y produce como resultado CUADRADO(x) es 16.
PR(CUADRADO(x));
PR(CUADRADO(x+2)); PR(1OO/CUADRADO(2)); PR(CUADRADO (++x)) ;
}
Allí donde aparezca CUADRADO(x) en el programa será sustituido por x*x. Lo que diferencia este ejemplo de los anteriores es que podemos em plear otros símbolos distintos de x cuando usemos la macro. La ‘x’ de la de finición de la macro se sustituye por el símbolo empleado en la llamada a la macro en el programa. Por tanto, CUADRADO(2) se sustituye por 2*2, de forma que x actúa realmente como un argumento. Sin embargo, como veremos pronto, un argumento de una macro no actúa exactamente como
al ejecutarse el programa. Vamos a zanjar el asunto de las comillas de una vez por todas. Si la defi nición de la macro incluye un argumento entre comillas, ese argumento será sustituido por el preprocesador; pero después de eso ya no se efectuará nin guna sustitución ulterior aunque el argumento fuera otra macro (queda pro tegido por las comillas de la primera definición). En nuestro ejemplo, x pasa a ser CUADRADO (x), y así se queda. En la siguiente línea de salida tenemos un resultado ligeramente descon certante. Recuerde que le hemos asignado a x el valor 4. Esto le podría indu cir a pensar que CUADRADO(x + 2) fuera 6*6 = 36, pero el resultado di ce que es 14, que, además, resulta que no es un cuadrado demasiado perfec to. La causa de esta broma del ordenador es muy simple; como ya dijimos antes, el preprocesador no efectúa ningún cálculo, se limita a cambiar tiras 319
de caracteres. Allí donde en nuestro programa aparezca x, el preprocesador colocará x + 2; por tanto, x*x se convierte en x + 2*x + 2 La única multiplicación es 2 * x. Si x vale 4, entonces el resultado de la expresión es 4 + 2*4 + 2 = 4 + 8 + 2 = 1 4
La lección que tenemos que sacar de aquí es que no debemos escatimar los paréntesis y colocarlos generosamente, para asegurarnos que las asocia ciones y operaciones se hacen en la forma debida. Pero incluso esta precaución falla en el último ejemplo de lo que podría mos llamar “expresión masoquista de macros”: cuadrado (++x)
se convierte en ++x*++x
y la x se incrementa dos veces, una antes y otra después de la multiplicación: + +x* + + x = 5*6 = 30
Este ejemplo nos revela una diferencia capital entre una llamada a una función y la expansión de una macro. Una llamada a una función pasa el valor del argumento a la función durante la ejecución del programa. La ex pansión de una macro pasa la tira de caracteres del argumento al programa antes de la compilación. Son procesos distintos en momentos diferentes. ¿Podría arreglarse nuestra definición para que CUADRADO(x + 2) fuera igual a 36? Faltaría más. Lo único que necesitamos son más paréntesis: #define CUADRADO(x) ((x)*(x))
Según lo anterior, CUADRADO(x + 2) se convierte en (x + 2) * (x + 2) y obtenemos el resultado apetecido, ya que en los caracteres de repuesto van incluidos los paréntesis. Espere; no eche las campanas al vuelo hasta ver la siguiente línea de sali da del programa: 100/CUADRADO(2) se convierte en 100/2 * 2 Por causa de las leyes de precedencia, esta expresión se evalúa de izquier da a derecha, con lo que obtenemos: (100/2) * 2 = 50 * 2 = 100 Este hacer y deshacer puede eliminarse definiendo CUADRADO(x) de la siguiente forma: #define CUADRADO(x) (x)*(x)
que resulta en 100/(2 * 2) que en su momento se calcula como 100/4 = 25 Para obtener lo que esperábamos en ambos ejemplos, tenemos que hacer la siguiente definición: #define CUADRADO(x) (x*x)
(Ya que el orden de las operaciones no está prefijado, algunos compila dores realizarán el producto 6*5, pero el resultado es el mismo.) La única solución a este problema es abstenerse de emplear + + x como argumento de una macro. Puede, sin embargo, usarse como argumento de una función, ya que sería calculado (5) y a la función le llega el valor 5.
¿Macros o funciones? En muchos casos podemos encontrarnos en la incertidumbre de emplear una macro con argumentos o una función. En general, no hay una línea divi soria nítida y sencilla, pero conviene hacer algunas consideraciones. Es preciso estar mucho más atento cuando se usan macros que cuando se emplean funciones (son más traicioneras, como acabamos de ver). Algu nos compiladores limitan la definición de una macro a una línea, y quizá fuera mejor así. Si quiere un consejo, actúe como si su compilador no permitiera definiciones en varias líneas. La elección entre macros y funciones es otra forma de la lucha entre tiempo y espacio. Las macros producen programas de mayor extensión, ya que aña dimos una sentencia al programa. Si emplea la macro 20 veces, cuando el preprocesador termine su trabajo, su programa contendrá 20 nuevas senten cias. Si, por el contrario, emplea una función 20 veces, su programa sola mente tendrá una vez el cuerpo de la función; por tanto, ocupará un menor espacio. Como contrapartida, el control del programa deberá saltar al punto de comienzo de la función y, una vez terminada, retornar al punto de donde salió; este proceso es más lento que seguir el curso normal sin saltos. Una ventaja adicional de las macros es que, debido a que actúan sobre caracteres y no sobre los valores que representan, son independientes del ti po que tengan las variables. Así, nuestra CUADRADO(x) funcionará igual con variables int o float. Generalmente, se suelen emplear macros para las funciones sencillas tal como: #define MAX(X,Y) ((X) > (Y) ? (X) : (Y)) #define ABS(X) ((X) < O ? -(X) : (X)) #define ESSIGNO(X) ((X) == '+' ||(X) == '-'? 1 : O)
321
(La última macro tiene un valor 1 (verdad) si x es un carácter de signo algebraico.) Como puntos interesantes podemos señalar: 1. No hay espacios en una macro, pero puede haberlos en los caracteres de repuesto. El preprocesador supone que la macro termina cuando encuentra un blanco, de forma que todo lo que se encuentre detrás del primer espacio irá a parar a los caracteres de repuesto.
rectorio estándar del sistema. Las comillas le dicen que lo busque en su direc torio (o en algún otro si se le añade el nombre del fichero) en primer lugar; si no lo encuentra, en el directorio estándar. #include busca en los directorios del sistema. #include "tierno, h" busca en el directorio en el que estétrabajando #include "/usr/biff/p.h" buscaen el directorio /usr/biff
Si estamos trabajando en un microordenador típico, las dos formas son iguales, y el preprocesador buscará en el disco que le indiquemos #include "stdio.h" #include #inciude "a:stdio.h"
Figura 11.2
Espacio erróneo en la definición de una macro
2. Emplee paréntesis para rodear a cada argumento y a la definición co mo un todo. Esto asegura que los términos quedarán agrupados como queremos en una expresión tal como: tenedores = 2 * MAX(invitados + 3, último);
3. Hemos utilizado letras mayúsculas para los nombres de las macros con argumentos. Este convenio no está tan generalizado como el de usar mayúsculas para las constantes simbólicas. Una buena razón para ha cerlo así es que se distinguen mejor de las funciones en el listado del programa y le mantienen alerta contra posibles efectos colaterales. Suponga que ha desarrollado algunas “funciones-macro” que le resultan cómodas. ¿Tendrá que teclearlas en cada nuevo programa que escriba? De ninguna forma. Para hacer eso existe otro comando: #include. Aunque ya nos lo hemos encontrado antes, no estará de más echarlo un vistazo ahora.
Inclusión de ficheros: #include Cuando el preprocesador encuentra un comando #include busca el fiche ro que atiende por el nombre que está situado detrás y lo incluye en el fichero actual. El nombre del fichero puede venir de dos formas: #include nombre del fichero entre paréntesis de ángulo #include "mifichero,h" nombre del fichero entre comillas
Si estamos trabajando en UNIX, los paréntesis de ángulo le indican al preprocesador que busque el fichero en un (también puede haber varios) di
busca en el disco de trabajo busca en el disco de trabajo busca en el disco a
¿Para qué incluir ficheros? Porque tienen la información que necesita. El fichero stdio.h, por ejemplo, contiene generalmente las definiciones de EOF, getchar( ) y putchar( ). Las dos últimas, definidas como macros con argu mentos. El sufijo .h se suele emplear para ficheros de encabezamiento (header), es decir, con información que debe ir al principio del programa. Los ficheros cabecera —como también se llaman— consisten, generalmente, en senten cias para el preprocesador. Algunos, como stdio.h, vienen con el sistema, pero pueden crearse los que quiera para darle un toque personal a sus pro gramas. Ficheros de encabezamiento: ejemplo
Suponga que le guste emplear valores booleanos. Dicho de otra manera, a usted le gustaría emplear CIERTO y FALSO en lugar de 1 y 0, respectiva mente. Entonces podría crear un fichero que se llamase, por ejemplo, bool.h, que contuviera estas definiciones: /* fichero bool.h define BOOL int #define CIERTO 1 #define FALSO 0
*/
#
Un programa que empleara estas definiciones podría ser: /* contador de espacios en blanco */ #include #include “bool.h" main() { char caracter; int contador = 0; BOOL espacio() ; 323
}
while ((ch = getchar() ) != EOF) if (espacio(caracter)) contador++; printf("He contado %d espacios en blanco. \n", contador) ;
BOOL espacio(c) char c;
{ if (c == ' ' || c == '\n' || c == '\t') return(CIERTO) ; else return(FALSO);
}
Observaciones al programa:
1. Si las dos funciones de este programa (main y espacio) se compilaran por separado, tendría que emplear el comando #include “bool.h” en cada una de ellas. 2. La expresión if( espacio(carácter)) es la misma que if( espacio(carácter) == VERDAD), ya que espacio(carácter) toma los valores CIERTO o FALSO. 3. Realmente, no hemos creado un nuevo tipo BOOL, ya que BOOL signifi ca lo mismo que int. Al definir la función con el “tipo” BOOL, lo único que indicamos es que realiza operaciones lógicas y no aritméticas. 4. El empleo de una función para la realización de las comparaciones lógi cas puede contribuir a la claridad de un programa. También puede acor tarlo si la comparación se realiza en varios lugares. 5. Podríamos haber empleado una macro en lugar de una función para defi nir espacio( ). Muchos programadores suelen desarrollarse sus propios ficheros cabece ra para sus programas. Algunos podrían estar dirigidos a un determinado trabajo, y otros podrían emplearse con casi cualquier programa. Como los ficheros incluidos pueden contener otros comandos #include, pueden crear se ficheros cabecera de una forma estructurada. Sea el ejemplo siguiente: /* encabezamiento (header) mifichero.h
*/
#include #include "bool.h" #include "func.h" #d efin e S I 1 #defin e N O 0
En primer lugar, le recordamos que el preprocesador C reconoce los co mentarios entre /* y */, de forma que podremos incluir comentarios en los ficheros.
En segundo lugar, hemos incluido tres ficheros. El tercero contendrá, se guramente, la definición de algunas macro-funciones de uso común. Por último, hemos definido SI como 1, mientras que en bool.h habíamos definido CIERTO también como 1. No hay problema en esto, podemos usar SI y CIERTO en el mismo programa; ambos serán reemplazados por 1. Si añadimos la línea #define CIERTO 2
al fichero, habría conflicto entre ambas definiciones, y el preprocesador con sidera la última únicamente. Algunos, incluso, le avisarán de que CIERTO ha sido redefinido. El comando #include no está restringido únicamente a ficheros cabecera. Si tiene un fichero llamado integral.c, que contiene una función útil, puede emplear #include "integral.h”
para introducirlo en su programa y compilarlos juntos. Los comandos #include y #define son, con mucho, los más empleados del preprocesador C. Comentaremos el resto de los comandos un poco más de pasada.
Otros comandos: #undef, #if, #ifdef, #ifndef, #else y #endif Estos comandos se emplean típicamente cuando se construyen programas de gran tamaño por medio de bloques bien diferenciados. Le permiten al pro gramador dejar sin efecto definiciones anteriores y producir ficheros que pue den compilarse de distintas formas. El comando #undef deja sin efecto la última definición de una macro. #define GRANDE 3 #define ENORME 5 #undef GRANDE /* ahora GRANDE no esta definido */ #define ENORME 10 /* ENORME se redefine como 10 */ #undef ENORME /* ENORME vuelve a valer 5 */ #undef ENORME / * ENORME esta ahora indefinido */
Evidentemente no esperamos que un fichero como el anterior sea de mu cha utilidad; pero suponga que dispone de un fichero #include de gran tama ño en su sistema y que le interesa usarlo, aunque tenga que cambiar tempo ralmente algunas de sus definiciones en algún punto de su programa. En lugar de reajustar todo su programa, puede incluirlo en su programa direc tamente, y rodear la zona conflictiva con los #define y #undef que sean necesarios. 325
Otro ejemplo podría ser el siguiente: suponga que está trabajando en un conjunto de programas en colaboración con otros programadores. Quiere de finir una macro, pero no está seguro si su definición será compatible con otras realizadas por sus compañeros. Para evitar problemas de la forma más sen cilla, deje sin efecto sus definiciones en cuanto termine su zona de utilidad, y, si estaban definidas anteriormente, volverán a recuperar su valor. Los comandos restantes que mencionamos le permiten realizar una com pilación bajo determinadas condiciones. Aquí va un ejemplo:
Hasta ahora hemos aprendido Cómo definir constantes simbólicas: #define DEDOS 10 Cómo incluir otros ficheros: #include “etrusco.h” Cómo definir macro-funciones: #define NEG(X) (-(X)) Cuándo usar constantes simbólicas: a menudo Cuándo usar macro-funciones: a veces Los peligros de las macro-funciones: efectos colaterales
#ifdef VAQUERO
Cuestiones y respuestas
#include "caballo.h" /* se realiza si VAQUERO esta definido */ #define ESTABLOS 5 #else #include "vaca.h" /* se realiza si no esta definido */ #define ESTABLOS 15 #endif
El comando #ifdef indica que si el identificador que le sigue (VAQUE RO) ha sido definido por el preprocesador, entonces se ejecutan todos los comandos hasta el siguiente #else o #endif. Si encuentra primero un #else, entonces se ejecutan los que se encuentren desde #else hasta #endif si el iden tificador no está definido. La estructura es similar a la if-else del C. La principal diferencia reside en que el preprocesador no reconoce el método { } de limitar un bloque; por tanto, emplea #else (si lo hay) y #endif (que debe estar) para delimitar los bloques de comandos. Estas estructuras condicionales pueden anidarse en más de un nivel. Los comandos #ifndef e #if pueden emplearse junto con #else y #endif de la misma forma. #ifndef pregunta si el identificador no está definido; es el complementario de #ifndef. El comando #if se parece más al if de C; es seguido por una expresión constante, que se considera cierta si no es 0: #if SISTEMA=="IBM" #include "ibm.h" #endif
Un uso de esta “compilación condicional” es hacer que un programa sea más portátil. Cambiando unas pocas definiciones claves al principio de un fichero puede conseguir la inclusión de diferentes ficheros y la definición de los parámetros correspondientes a distintos sistemas. Estos pocos ejemplos ilustran la extraordinaria capacidad del C para el control de los programas.
Cuestiones 1. Sean los siguientes grupos de una o más macros seguidas por una línea de progra ma que las emplea. ¿Qué resulta de la expansión de la macro? ¿Será válida para el compilador? a. #define UPD 12 /* unidades por docena */ cantidad = UPD * docenas; b. #define BASE 4 #define ALTURA BASE+BASE superf = BASE * ALTURA C. #define SEIS = 6; nex = SEIS; d. #define NUEVO(X) X + 5 y = NUEVO(y); valor = NUEVO(valor) * tasa; est = NUEVO(valor) / NUEVO(y); nilp = tasa * NUEVO ((-valor) ; 2. Corrija la definición de 1.d para hacerla más correcta. 3. Defina una macro-función que devuelva el menor de dos valores. 4. Defina una macro que sustituya a la función espacios(c) en el programa que cuenta caracteres blancos. 5. Defina una macro-función que imprima la representación y los valores de dos ex presiones enteras.
Respuestas
1. a. cantidad = 12 docenas; válida. b. superf = 10 * 10 + 10; válida, pero si se quería calcular 4 * (4 + 4), debería haberse definido así: #define ALTURA (BASE + BASE) c. lados ==6;; no válida; aparentemente el programador olvidó que estaba hablando con el preprocesador, no con el compilador C. d. y = y + 5; válido valor = valor + 5 * tasa; válido, pero no lo que se quería, probablemente, estim = valor + 5/y + 5; igual que antes, def = tasa * -valor + 5; igual que antes. 2. #define NUEVO((X) + 5) 3. #define MIN(X,Y) ((X) < (Y) ? (X) : (Y)) 4. #define ESPACIO (C) ((C) == ‘ ’ || (C) == ‘ \ n ’
||
(C) == ‘ \t’) 327
#define IMPR2(X,Y) printf(“X es %d e Y es %d. n”, X,Y)
Como X e Y no están expuestas a otras operaciones (como multiplicaciones) en esta ma-
cro, podemos ser más avaros en paréntesis.
Ejercicio 1. Empiece a crearse un fichero cabecera de definiciones para el le gustaría tener.
12 Arrays
y punteros En este capítulo encontrará: • Arrays • Inicialización y clase de almacenamiento • Punteros a arrays • Funciones, arrays y punteros • Suplantación de arrays por punteros • Operaciones con punteros • Arrays multidimensionales • Punteros y arrays multidimensionales • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios
331
Arrays y punteros
valores por defecto cuando no se definen explícitamente. Sean, por ejemplo, las siguientes declaraciones: /* algunas declaraciones de arrays * / int temp[365]; /* array external de 365 enteros */ main()
CONCEPTOS Arrays Arrays multidimensionales Inicialización de arrays Punteros y operaciones con punteros La relación array-puntero
OPERADORES & *(unario)
float lluvia[365] ; /* array automático de 365 float */ static char codigo[12]; /* array static de 12 char */ extern temp[]; /* array external; tamano indicado */
Recuerde también que los corchetes ([ ]) identifican a temp y a los demás como arrays, indicando el número encerrado entre ellos cuántos elementos tiene. Para indicar un elemento de un array, usaremos su número de orden (su subíndice, en notación algebraica) denominado también índice. El pri mer elemento tiene como índice el 0. Por tanto, temp[0] es el primer elemen to de temp, y temp[364] es el último. Como lo anterior ya está manido, veamos algo nuevo. Inicialización y clase de almacenamiento
En C, los arrays y los punteros mantienen relaciones íntimas (en el buen sentido), de modo que los trataremos juntos. De todas formas, antes de in troducirnos en tan escabroso asunto, repasaremos y aumentaremos nuestros conocimientos sobre los arrays.
De momento, lo único que conocemos de un array es que está compuesto por una serie de elementos del mismo tipo. En la declaración de las variables le decimos al compilador cuándo queremos un array. Por su parte, el compi lador necesita conocer de un array lo mismo que de cualquier otra variable (a las “otras” variables las denominaremos de ahora en adelante “escala res”): su tipo y su clase de almacenamiento. Pero, además, el compilador de be conocer el número de elementos que forman el array. En cuanto al tipo y clase de almacenamiento, no hay ningún problema; los arrays pueden tener los mismos que cualquier variable escalar; es más, se les aplican los mismos
Bastante a menudo se emplean arrays para guardar una serie de datos uti lizados por un programa. Por ejemplo, podemos guardar los días que tiene cada uno de los meses en un array de 12 elementos. Para este tipo de aplica ciones, sería conveniente disponer de una forma sencilla y cómoda de colo car los valores en el array al principio del programa. Ya habrá supuesto que el C la tiene, pero sólo para aquellos arrays que permanezcan de forma con tinua en memoria (es decir; externos o estáticos). Veamos cómo se hace. Sabemos que para inicializar una variable el método es declararla con un valor: int flix = 1 ; float flax = PI*2;
donde, es de suponer, hemos definido anteriormente una macro llamada PI. ¿Podemos hacer algo parecido con los arrays? La respuesta es nuestra favo rita, sí y no: Los arrays clase EXTERN o STATIC pueden inicializarse. Los arrays clase AUTOMATIC o REGISTER, no. Antes de tratar de inicializar un array, veamos lo que tienen si no coloca mos nada en ellos /* a ver que sale */ main() { int bas[2]; /* array automatico */ static int ura[2]; /* array estatico */ printf("%d %d\n", bas[1], ura[1]);
} 333
La salida es 525 0
Este ejemplo nos muestra la norma siguiente: si no se les da ningún valor, los arrays extern y static son inicializados a 0, mientras que los automatic y register contendrán lo que hubiera en la parte de memoria donde les haya tocado caer. Bueno, ya sabemos cómo inicializar un array de clase extern o static a 0: simplemente, no haciendo nada. Normalmente esto no nos servirá de mu cha ayuda, si lo que queremos es colocar otros valores, quizá el número de días de cada mes, quizá otra cosa. Para casos como éste, podemos hacer lo siguiente:
anterior, pero con una lista demasiado corta (más exactamente, demasiados corta): /* dias del mes */ int dias[12] = {31,28,31,30,31,30,31,31,30,31}; main()
{ int indice; extern int dias[]; /* declaracion opcional */ for (indice = 0; indice < 12; indice++) printf("El mes %d tiene %d dias. \n", indice + 1, dias [indice]);
} Esta vez la salida sería:
/* dias del mes */ int dias[12] = {31,28, 31,30, 31,30, 31,31,30, 31,30, 31 }; main()
{ int indice; extern int dias[]; /* declaracion opcional */
}
for (indice = 0; indice < 12; indice++) printf("El mes %d tiene %d dias. \n", indice + 1, dias[indice]);
La salida será:
Como puede observar, el compilador no tiene problemas. Cuando la lista es corta, sigue con su manía de inicializar a 0 los restantes elementos. Por el contrario, el compilador es inexorable si la lista es demasiado lar ga. Este tipo de altruismo está considerado como un ERROR. De todas for mas, no es necesario exponerse a quedar en ridículo por el simple hecho de que el compilador sepa contar mejor que usted. Demuestre su superioridad dejando al compilador que encuentre el tamaño del array: /* dias del mes */ int dias[12] = {31,28,31,30, 31,30,31,31,30,31}; main()
{ int indice; extern int dias[]; /* declaracion opcional */
Realmente no es un programa muy complicado, sólo se equivoca una vez cada cuatro años. Al definir días[ ] fuera de la función, su clase de almace namiento es externa. Para inicializarlo colocamos la lista de valores entre lla ves y separados por comas. El número de elementos de la lista debería coincidir con el tamaño del array. ¿Qué pasa si nos equivocamos al contar? Probemos con el ejemplo
}
for (indice = 0; indice < sizeof dias/(sizeof(int)); indice++) printf("El mes %d tiene %d dias. \n", indice + 1, dias[indice]);
Hay dos puntos interesantes en este programa. Primero, si emplea corchetes vacíos cuando inicialice un array, el compi lador contará el número de términos de la lista, y ése será el tamaño del array. 335
Segundo, observe detenidamente la expresión de control del bucle for. Co mo no nos fiamos (además, con razón) de nuestra habilidad con los núme ros, dejamos al ordenador que cuente por nosotros y calcule el tamaño del array. El operador sizeof nos da el tamaño, en bytes, del objeto o tipo que coloquemos a continuación (ya mencionamos algo parecido en el capítulo 3). En nuestro sistema, cada elemento de tipo int ocupa 2 bytes, de forma que podemos dividir el número total de bytes por 2 para saber el número de ele mentos; pero otros sistemas pueden tener enteros de distinta longitud; por tanto, por mor de la generalidad, dividimos por sizeof(int). Aquí está el resultado de nuestro programa:
punteros nos permiten aproximarnos a la forma de trabajo de la máquina. Esto hace que los programas que utilizan punteros puedan ser más eficien tes. En particular, los punteros hacen más rápido el trabajo con arrays. Real mente, nuestra notación como arrays es un método disfrazado de empleo de punteros. Un ejemplo de esta torva maniobra es que el nombre de un array es tam bién un puntero dirigido al primer elemento del array. En otras palabras, si antifaz[ ] es un array, entonces la igualdad antifaz == &antifaz[0]
es cierta, y ambos términos representan la dirección de memoria del primer elemento. (Recuerde que & es el operador dirección.) Ambos términos son punteros constantes, ya que no pueden cambiar su valor a lo largo del pro grama. Sin embargo, pueden ser asignados a una variable puntero, y pode mos cambiar el valor de una variable, como demuestra el siguiente ejemplo. Observe lo que ocurre cuando sumamos un número a una variable puntero. /* adicion de punteros */ main()
{ ¡Cielos! Sólo dimos 10 valores. En cualquier caso, nuestro método de de jar al programa el trabajo de encontrar el tamaño del array nos ha evitado una pequeña catástrofe: escribir datos fuera del mismo. Existe otro método más corto de inicialización de arrays, pero como sólo funciona con tiras de caracteres, lo dejaremos para el siguiente capítulo. Por último, debemos indicar que se puede asignar un valor a un elemento del array, independientemente de su clase de almacenamiento. Por ejemplo, el siguiente trozo de programa asigna números pares a un array de clase auto-
matic:
int fechas[4] *pent, indice; float facturas, *pflot; pent = fechas; / * asigna la direccion del array al puntero pflot = facturas; for (indice = 0; indice < 4; indice++) printf("punteros + %d: %10u %10u\n", indice, pent+indice, pflot+indice);
}
La salida es:
/* asignacion de arrays */ main()
{ int contador, pares[50]; for (contador = 0; contador < 50; contador++) pares[contador] = 2 * contador;
}
Punteros a arrays Los punteros, como recordará del capítulo 9, nos proporcionan un méto do simbólico para usar direcciones de memoria. Como las instrucciones del hardware de los ordenadores emplean abundantemente estas direcciones, los
En la primera línea escribe las direcciones de comienzo de los dos arrays; en la siguiente nos da el resultado de sumar 1 a las direcciones, y así sucesiva mente. ¿Qué? ¿56014 + 1 = 56016? ¿56026 + 1 = 56030? ¿Nos toma el pelo o es muy listo? Nuestro sistema direcciona cada byte individualmente, pero el tipo int usa dos bytes y el float cuatro. Lo que ocu rre es que cuando le decimos “suma 1”, el C suma no un byte, sino una uni337
*/
dad de almacenamiento del tipo al que pertenece el puntero. Para arrays esto significa que la dirección cambia al siguiente elemento, no al siguiente byte. Esta es una de las razones por las que tenemos que declarar a qué tipo de elementos se va a referir el puntero, ya que la dirección no es suficiente; el computador necesita saber cuántos bytes se usan para guardar ese objeto. (Incluso para variables escalares es necesario, si no la operación *pt nos de volvería un valor erróneo.)
La relación entre arrays y punteros permite que podamos usar ambas no taciones indistintamente al escribir un programa. Un ejemplo típico es cuan do tenemos una función, uno de cuyos argumentos es un array.
Funciones, arrays y punteros Podemos encontrarnos arrays en una función en dos sitios. En primer lu gar pueden estar declarados dentro del cuerpo de la función; el segundo caso es que aparezcan como argumentos. Todo lo que llevamos dicho hasta ahora se corresponde con arrays del primer tipo; ahora entraremos en detalle con los segundos. En el capítulo 10 dejamos pendiente esta cuestión. Ahora que sabemos más acerca de los punteros, podemos continuar. Empezaremos por observar el esqueleto de un programa (¡atención a las declaraciones!). /* array como argumento */ main() { int edad[50]; /* un array de 50 elementos */ convierte(edad);
... } convierte(primaveras) int primaveras[]; /* que tamaño tiene este array? */ Figura 12.1
Un array y suma con punteros
Como resultado de la habilidad del C, las siguientes igualdades son ciertas: fechas + 2 == &fechas[2] /* misma direccion */ *(fechas + 2) == fechas[2] /* mismo valor */
{ ... } El array edad tiene, obviamente, 50 elementos. Pero, ¿y el array prima veras? ¡Sorpresa!, ¡el array primaveras no existe! La declaración int primaveras[];
no crea un array, sino un puntero. Veamos por qué se hace así. Esta es nuestra llamada a la función:
Estas relaciones resumen la estrecha conexión entre arrays y punteros. Vie nen a decir que podemos usar un puntero para identificar un elemento de un array y tomar su valor. En esencia, son dos formas de nombrar la misma cosa; realmente, el compilador convierte la notación de array a punteros, así que esta última está más próxima a la realidad que la primera. Incidentalmente, no es lo mismo *(fechas + 2) que *fechas + 2. El ope rador valor (*) tiene mayor jerarquía que la suma ( + ) , así que la última ex presión significa (*fechas) + 2: *(fechas + 2 ) *fechas + 2
/* valor del tercer elemento de fechas */ /* suma 2 al valor del primer elemento */
convierte(edad);
El argumento es edad; como recordará, edad es un puntero al primer ele mento de los 50 del array, por tanto, la llamada de la función pasa a la fun ción convierte( ) un puntero (una dirección, en otras palabras). Si el argu mento de convierte( ) es un puntero, también hubiéramos podido escribir. convierte(primaveras) int *primaveras;
{ } 339
if (n > 0) {
Realmente, las declaraciones
for (indice = 0, suma = 0; indice < n; indice++) suma += array[indice]; return( (int) (suma/n) ); / * devuelve un entero */
int prim averas[];
}
int *prim averas;
else
}
son sinónimas. Ambas definen primavera como un puntero a un entero. La principal diferencia es que la segunda nos recuerda que primaveras[ ] apun ta a un array. ¿Qué ocurre con el array edad? Recuerde que, cuando empleamos un pun tero como argumento, la función trabaja con la variable apuntada (de la fun ción donde se produce la llamada); por tanto, todas las operaciones que afecten a la variable primaveras realmente estarán trabajando sobre el array edad de la función main( ). Veamos cómo funciona esto. Primero, la llamada a la función inicializa primaveras de forma que apunte a edad[0]. Suponga ahora que en alguna parte de convierte( ) tenemos la expresión primaveras[3]. Como vimos an tes, eso es lo mismo que decir *(primaveras + 3). Pero si primaveras apunta a edad[0], entonces primaveras + 3 apunta a edad[3], O sea, que *(primaveras + 3) sea equivalente a edad[3]. Coloque ahora esta cadena de relaciones en un orden lógico, y la conclusión que obtenemos es que cambiar primaveras[3] es lo mismo que cambiar *(primaveras + 3), que, a su vez, es lo mismo que cambiar edad[3]. Y esto es lo que avisamos: si manipulamos las primaveras se afecta la edad (real como la vida misma). En resumen, cuando emplee el nombre de un array como argumento de una función, le pasa un puntero. La función usa este puntero para realizar cambios en el array original del programa del cual partió la llamada. Veamos un ejemplo.
printf("No hay array.\n"); return(0);
}
}
No es demasiado difícil convertir este programa para que use punteros. Declare pa como puntero a un entero (int). A continuación cambie el ele mento del array (array[indice]) por el valor correspondiente: *(pa + indice). /* usa punteros para calcular la media de un array de n enteros * / int media(pa,n) int *pa, n; { int indice; long suma; /* si hay muchos int puede salir un long
*/
if (n > 0)
{
for (indice = 0, suma = 0; indice ( n; indice++) suma += *(pa + indice); return((int) (suma/n) ); /* devuelve un entero */
} else
{
printf("No hay array. \n") ; return(0);
Suplantación de arrays por punteros Escribiremos una función que emplee arrays. A continuación la reforma remos para usar punteros. Sea la siguiente función que halla la media de una array de enteros. La entrada es el nombre del array y el número de sus elementos. La salida es la media, que se devuelve a través de un return. La sentencia en donde se llama a la función podría ser algo como:
}
}
Fácil, ¿verdad?; pero, ¿no tendremos que cambiar la llamada?; después de todo, números en media(números,n elem) era el nombre de un array. No, no es necesario ningún cambio, ya que el nombre de un array es un puntero. Como se discutió en la sección anterior, las declaraciones int pa[];
printf("La media de estos valores es %d. \n”, media(numeros, nelem)); /* calcula la media de un array de n enteros */ int media(array, n) int array[], n;
{
int indice; long suma; /* si hay muchos int puede salir un long */
e int *pa;
son idénticas; en efecto, ambas dicen que pa es un puntero. Podríamos usar la primera declaración y emplear *(pa + indice) en el programa. 341
Aquí está la salida: ¿Cómo funciona la versión con punteros desde un punto de vista concep tual? Un puntero señala al primer elemento, y el valor guardado allí es aña dido a suma; después, el elemento señalado es el siguiente (sumamos 1 al pun tero), y el valor almacenado allí es añadido a suma, y así, sucesivamente, hasta el último. Si lo considera con detenimiento, esto es exactamente lo que hace la version con arrays, donde el subíndice actúa como el dedo acusador que va señalando a cada elemento. Bueno, ya sabemos cómo hacer el mismo trabajo con arrays y punteros, y ahora podemos preguntarnos: ¿cuál usar? En primer lugar, a pesar de que los punteros y los arrays están muy relacionados, aún tienen diferencias. Los punteros son más generales y más potentes, pero muchos usuarios (por lo menos al principio) encuentran más familiares y sencillos los arrays. Ade más, no hay una forma sencilla, trabajando con punteros, de declarar el ta maño de un array. La situación más típica en donde se pueden emplear am bos indistintamente es la que hemos visto: una función que opera sobre un array definido en algún otro lugar. Le sugerimos que use la opción que más le guste. La principal ventaja de emplear punteros en casos como éste es la de familiarizarse con ellos, de forma que le sea más fácil usarlos cuando ten ga que usarlos.
Este ejemplo muestra las cinco operaciones básicas que podemos realizar con variables punteros. 1. ASIGNACION: Podemos asignar una dirección a un puntero. De una forma normal se empleará con el nombre de un array o con el opera dor dirección (&). En nuestro ejemplo, asignamos a punt1 la dirección de inicio del array urn; esta dirección ha caído en la célula de memoria número 18. (En nuestro sistema, las variables static se colocan en las direcciones bajas.) En la variable punt2 colocamos la dirección del ter cer y último elemento, urn[2].
Operaciones con punteros ¿Qué es lo que podemos hacer con los punteros? El lenguaje C le ofrece cinco operaciones básicas, y el programa siguiente le muestra estas posibili dades. Para mostrar los resultados de cada operación imprimiremos el valor del puntero (la dirección donde apunta), el valor almacenado en dicha direc ción y la dirección del puntero (donde se guarda su valor). /* operaciones con punteros */ #define PR(X) printf("X = %u, *X =%d, &X = %u\n”, X, *X, &X) /* imprime el valor del puntero (una direccion), el valor */ /* almacenado en esa direccion, y la propia direccion del */ /* puntero. */ main() { static int urn[] = {100,200,300}; int *punt1, *punt2; punt1 = urn; /* asigna una direccion al puntero */ punt2 = &urn[2]; /* idem */ PR(punt1); /* vease definicion macro arriba */ punt1++; /* incrementa el puntero */ PR(punt1) ; PR(punt2); ++punt2; /*s e sale del limite del array */ PR(punt2); printf("punt2 - punt1 = %u\n", punt2 - punt1);
2. VALOR GUARDADO EN UNA DIRECCION: El operador * nos da el valor almacenado en la posición de memoria apuntada. Así, *punt1 vale, inicialmente, 100, que es el valor guardado en la posición 18. 343
3. DIRECCION DE UN PUNTERO: Como todas las variables, los pun teros tienen una dirección y un valor. El operador & nos dice dónde está almacenado el puntero. En nuestro ejemplo, punt1 está almace nado en la dirección 55990. El valor contenido en esa célula de memo ria es 18, la dirección de urn. 4. INCREMENTO DE UN PUNTERO: Podemos hacer esta tarea como una suma normal o por medio del operador de incremento. Si incre mentamos un puntero, éste se moverá al siguiente elemento del array. Por tanto, punt + + incrementa el valor numérico de puntl1 en 2 (un int ocupa 2 bytes) y hace que punt1 señale a urn[1] (véase la figura siguiente). Ahora punt1 tiene el valor 20 (la siguiente dirección del array) y punt1 tiene el valor 200, el de urn[1]. Obsérvese que la direccion de punt1 sigue siendo 55990. Después de todo, una variable no se mueve de su sitio sólo por el hecho de cambiar su valor. Por supuesto, también se puede decrementar un puntero. Hay que hacer notar algunos puntos peligrosos. El ordenador no le sigue la pista a un puntero; por tanto, no sabe si está apuntando dentro del array. La operación + + punt1 hizo que punt2 se moviera otros dos bytes, y ahora apunta a lo que haya detrás del array.
es un expresión muy atractiva). Sin embargo, sí pueden utilizarse en una suma normal. VALIDO
INVALIDO
punt1 + + ;
urn + + ; 3++; punt2 = urn + + ; x = y + 3+ +;
x++; punt2 = punt1 +2; punt2 = urn + 1;
5. RESTA: Dos punteros pueden restarse. Normalmente se utiliza con dos punteros que señalan dentro de un mismo array, para saber cuán tos elementos los separan. Observe que el resultado no es en bytes, si no en las mismas unidades que el tamaño del tipo del array. Estas operaciones nos abren muchas puertas. Los programadores de C suelen trabajar con arrays de punteros, punteros a funciones, arrays de pun teros a punteros, arrays de punteros a funciones y así sucesivamente. Por nues tra parte nos limitaremos a los usos básicos que ya hemos presentado. Su primer uso es pasar información a y desde una función. Ya hemos visto que una función sólo puede cambiar el valor de una variable en el programa que la llama si éste le pasa, como argumento, un puntero a la variable. En segun do lugar se emplean en funciones diseñadas para manipular arrays.
Arrays
F igu ra 12.2
Incremento de un puntero tipo int
Otro punto que no debe olvidarse es que sólo se pueden incremen tar variables, y no constantes, así que un puntero constante no puede cambiar su valor (esto puede parecer una perogrullada, pero + + urn
multidimensionales
Marisol Buendía de Verano es una meteoróloga que tiene el futuro bas tante nublado. La han puesto a analizar las lluvias de los últimos cinco años, correspondientes a cada mes, por medio de un ordenador. Una de las prime ras decisiones que tiene que tomar es la forma de representar los datos. Una alternativa es usar 60 variables, una para cada dato. (Mencionamos esta po sibilidad una vez en los capítulos anteriores, y sigue siendo tan estúpida co mo entonces.) El empleo de un array con 60 elementos sería un avance, pero quedaría mejor si pudieran mantener separados los datos de cada año. Se podrían usar 5 arrays de 12 elementos, pero es una solución liosa, y podría llegar a ser tan estúpida como la primera, si Marisol decide estudiar los últi mos 50 años en lugar de 5. Necesitamos alguna otra posibilidad más elegante. Una buena solución es usar un array de arrays. El array principal tendría 5 elementos, que serían, a su vez, arrays de 12 términos. He aquí la forma como se hace: static float lluvia[5] [12] ; 345
También podemos visualizar el array lluvia como una matriz de dos dimensiones con 5 filas y 12 columnas.
main() { static float lluvia[ANS][DOCE] = { {10.2, 8.1, 6.8, 4.2, 2.1, 1.8, 0.2, 0.3, 1.1, 2.3, 6.1, 7.4}, {9.2, 9.8, 4.4, 3.3, 2.2, 0.8, 0.4, 0.0, 0.6, 1.7, 4.3, 5.2}, {6.6, 5.5, 3.8, 2.8, 1.6, 0.2, 0.0, 0.0, 0.0, 1.3, 2.6, 4.2}, {4.3, 4.3, 4.3, 3.0, 2.0, 1.0, 0.2, 0.2, 0.4, 2.4, 3.5, 6.6}, {8.5, 8.2, 1.2, 1.6, 2.4, 0.0, 5.2, 0.9, 0.3, 0.9, 1.4, 7.2}
}; /* inicializacion de datos de lluvia en el periodo 1970 - 74 */ int anno, mes; float subtot, total; printf(" ANNO LLUVIA (cm.)\n\n"); for (anno = 0, total = 0; anno < ANS; anno++) { /* en cada anno suma la lluvia de todos los meses */ for (mes = 0, subtot = 0; mes < DOCE; mes++) subtot += lluvia[anno][mes]; printf("%5d %12.lf\n", 1970 + anno, subtot); total += subtot; /* calcula lluvia total del periodo */ } printf("\nEl promedio anual ha sido %.1f cm.\n\n", total/ANS); printf("PROMEDIOS MENSUALES:\n\n"); printf("Ene Feb Mar Abr May Jun Jul Ago Sep Oct "); printf(" Nov Dic\n"); for (mes = 0; mes < DOCE; mes++) { /* suma la lluvia de todos los annos en cada mes */ for (anno = 0, subtot = 0; anno < ANS; anno++) subtot("%4.1f ", subtot/ANS); } printf("\n");
Figura 12.3
Array bidimensional
}
Figura 12.4
Programa meteorológico
Al variar el segundo subíndice, nos moveremos a lo largo de una fila, y al variar el primero, el movimiento será vertical por una columna. En nues tro caso particular, el segundo subíndice representa los meses, y el primero, los años. Vamos a emplear este array bidimensional en un programa meteorológi co. Nuestro programa imprimirá la lluvia total caída cada año, la media anual y la media correspondiente a cada mes del año. (A nosotros también nos gus taría un programa que nos dijera el tiempo que iba a hacer mañana, pero tenemos problema con la función adivina( )). Para hallar la lluvia caída ca da año tenemos que sumar los datos de cada fila; para la media de lluvia de un determinado mes, sumaremos, en primer lugar, los datos de su columna correspondiente. La ordenación en forma de matriz hace que estos cálculos sean fáciles de visualizar y ejecutar. La figura 12.4 presenta el programa. /* calcula totales anuales, promedio anual y promedio mensual */ /* de datos pluviometricos en un periodo determinado */
#define DOCE 12 #define ANS 5
/* numero de meses del anno */ /* numero de annos a tratar */
Los principales puntos de interés son la inicialización y el esquema de cálcu lo. La inicialización es lo más complejo de ambos, así que veremos primero el esquema de cálculo. Para hallar el total correspondiente a un año, mantendremos fijo el año y correremos los meses. Este es el bucle for más interno de la primera parte 347
del programa. A continuación repetimos el proceso para el siguiente año. Este otro se corresponde con el for externo de la primera parte del programa. Las estructuras anidadas de bucles como ésta se presentan a menudo cuando se manejan matrices (arrays bidimensionales). Un bucle controla el primer su bíndice, y el otro, el segundo. Inicialización de un array bidimensional
Para la inicialización hemos incluido cinco series de números entre lla ves, que, a su vez, están encerradas entre otro par de llaves más externas. Los datos dentro del primer par de llaves se asignan a la primera fila del array, los de segundo par, a la segunda fila, y así sucesivamente. Las reglas que con trolan la situación cuando el número de datos no coincide con el tamaño del array son las mismas que se discutieron anteriormente. Si, por ejemplo, den tro del primer par de llaves solamente hay 10 números, sólo se inicializan los 10 primeros elementos de la primera fila; los otros 2 quedan inicializados a su valor por defecto, es decir, a 0. Si, por el contrario, hay demasiados, pro ducirá un error al compilarse el programa; no servirán para inicializar la se gunda fila. Podríamos haber puesto solamente el juego de llaves externo sin colocar los pares internos. Como el número de datos es el correcto, el resultado hu biera sido el mismo. Si, por el contrario, hubiéramos colocado menos datos, el array se hubiera ido llenando sin considerar la distribución en filas, hasta que se terminaran, inicializando el resto con ceros. Véase la figura 12.5.
Todo lo que hemos dicho para los arrays bidimensionales puede exten derse a los de mayor número de dimensiones. Podemos declarar un array de tres dimensiones de la siguiente forma: int solido[10][20][30] ;
Puede visualizar este array como 10 matrices (de 20 x 30) apiladas unas sobre otras. También puede considerarlo como un array de arrays de arrays, o sea, como un array de 10 elementos, cada uno de los cuales es un array de 20 elementos, que, a su vez, son arrays de 30 enteros. La ventaja de este método es que se puede extender fácilmente a un mayor número de dimen siones (¡a menos que usted sea capaz de visualizar objetos con cuatro o más dimensiones!). Volveremos a insistir en las bidimensionales.
Punteros y arrays multidimensionales ¿Cómo se relacionan los punteros con los arrays multidimensionales? Ve remos algunos ejemplos que nos conducirán a la respuesta. Sean las declaraciones int groucho[4][2] ; int *pint ;
/* arr ay de int de 4 fi las y 2 col umnas */ /* puntero a enter o */
si hacemos pint = groucho;
¿dónde apunta pint? Solución: apunta al elemento de la primera fila y pri mera columna;
Static int cuad[2][3] = { {5.6} {7,8}
groucho == &groucho[0] [0]
}; Pero, ¿dónde apunta pint + 1? ¿A groucho[0][1] (fila primera y colum na segunda) o a groucho[1][0] (fila segunda y columna primera)? Para res ponder a esta pregunta necesitamos saber cómo se almacenan los arrays bidi mensionales. Se guardan como arrays de una dimensión, es decir, como una serie de células de memoria consecutivas. El orden se determina variando pri mero el índice de la derecha. En nuestro ejemplo será: Static int cuad[2][3] = {5,6,7,8};
Figura 12.5
Dos formas de inicializar un array
groucho[ 0][0]gr oucho[0][1]groucho [ 1][0]gr oucho[1][1]groucho[ 2][0]
349
if (n > 0)
Primero se coloca la primera fila, luego la segunda, la tercera, y así suce sivamente. Por tanto: pint pint pint pint
== &groucho[0][0] + 1 == &groucho[0][1] + 1 == &groucho[1][0] + 1 == &groucho[1][1]
/* fila. 1, /* fila 1, /* fila 2, /* fila 2,
columna columna columna columna
groucho[2][1]
} else { printf("No hay array.\n"); return(0) ;
}
La media de la fila 0 es 5. La media de la fila 1 es 250. La media de la fila 2 es 50.
Funciones y
/* cajón de sastre */ main()
{
static int cosas[3][4] = {
/* funcion unidimensional, array bidimensional */ main()
{
{; }
static int cosas[3][4] = {
};
arrays multidimensionales
Suponga que queremos una función que trabaje sobre toda una matriz en lugar de “por rodajas”. ¿Cómo deberíamos elaborar las definiciones y declaraciones? En concreto, sea una función que maneje la matriz cosas[][] de nuestro ejemplo anterior. La función main( ) sería algo así como:
&groucho[0][0] &groucho[1][0] &groucho[2][0] &groucho[3][0]
Esto es más que una novedad. ¡Nos permite que una función diseñada para trabajar con arrays unidimensionales, la podamos usar con arrays multidimensionales! Si se siente escéptico frente a nuestras palabras, se lo de mostraremos empleando la función que definimos antes para calcular el valor medio de los elementos de una matriz.
{2, 4, 6, 8}, {100, 200, 300, 400}, {10, 20, 60, 90}
int fila; for (fila = 0; fila < 3; fila++) printf("La media de la fila %d es %d.\n", fila, media(cosas[fila],4));
}
}
La salida es:
Hemos descrito un array de dos dimensiones como un array de arrays. ¿Cuáles son los nombres de las cuatro filas, teniendo en cuenta que son arrays unidimensionales de dos elementos? El nombre de la primera fila es groucho[0], y el nombre de la cuarta es groucho[3]; le dejamos a usted el trabajo de des cubrir cómo se llaman las otras filas. Pero el nombre de un array es también un puntero a ese array que apunta al primer elemento. Esto significa que == == == ==
for (indice = 0, suma = 0; indice < n; indice++) suma += (long) array[indice] ; return( (int) (suma/n) );
1*/ 2*/ 1*/ 2*/
¿lo tiene?, pues entonces ¿qué apunta pint + 5? Correcto, a
groucho[0] groucho[1] groucho[2] groucho[3]
{
/* cosas[fila] es un array unidimensional de 4 elementos */
/* calcula la media de un array unidimensional */ int media(array,n) int array[], n;
{2, 4, 6, 8}, {100, 200, 300, 400}, {10, 20, 60, 90}
cajon(cosas);
La función cajón( ) coge cosas, que es un puntero al array completo, co mo argumento. Sin preocuparnos de lo que haga cajón( ), ¿cómo sería su encabezamiento? ¿Sería quizá así? cajon(cosas) int cosas[];
¿o estaría mejor así?
{ int indice; long suma;
cajon(cosas) int cosas[][]; 351
Cuestiones y respuestas Pues no, ambas están mal. El primer encabezamiento podría funcionar, pero cajón trataría a cosas como un vector (array unidimensional) de 12 ele mentos. La información de cómo están distribuidos (tres filas y cuatro co lumnas) se ha perdido. La segunda posibilidad falla en lo mismo. A pesar de que le indica al com pilador que el array tiene dos dimensiones, no le dice cómo se reparten los elementos. ¿Dos filas y seis columnas?, ¿dos columnas y seis filas?, ¿o qué? Hace falta más información. La solución es: cajon(cosas) int cosas[][4];
Así le indicamos al compilador que los elementos están agrupados en fi las de cuatro columnas. Los arrays de tiras de caracteres son un caso especial, ya que el carácter nulo (null) indica el fin de cada tira. Esto nos permite hacer declaraciones como: char *lista[] ;
Cuestiones
1. ¿Qué imprimirá este programa? #define PC(X,Y) printf("%c %c\n”, X, Y) char ref[] = { D, O, L, T}; main() { char *pint; int indice; for (indice = 0, pint = ref; indice < 4; indice++, pint++) PC(ref[indice], *pint);
} 2. En la cuestión anterior, ¿por qué ref se ha declarado antes de main( )? 3. ¿Cuál es el valor de *punt y *(punt + 2) en cada caso? a. int *pint; static int papa[43] = {12,21,121,212}; pint = papa;
b. float *pint ;
static float pepa[2][2] = { {1.0, 2.0}, {3.0, 4.0} }; pint = pepa[0];
Los arrays y punteros se usan muy frecuentemente con tiras de caracte res, y sus propiedades son algo particulares; los trataremos en el siguiente capítulo.
C. int *pint; static int pipa[4] = { 10023, 7}; pint = pipa;
d. int *pint;
Hasta ahora hemos aprendido
static int popa[2][2] = { 12, pint = popa[0] ;
14,
e. int *pint; Cómo declarar un array unidimensional: long no-id[200]; Cómo declarar un array bidimensional: short tablero[8][8]; Qué tipos de arrays podemos inicializar: external y static Cómo inicializar un array, static int sombreros[3] = {10,12,15}; Otra forma de inicializar: static int gorros[] = {3,56,2}; Cuál es la dirección de una variable: use el operador & Cuál es el valor apuntado por un puntero: use el operador * El significado del nombre de un array: sombreros = = &sombreros[0] Correspondencia entre arrays y punteros: si punt = sombreros, entonces punt + 2 = = & sombreros[2], y *(punt + 2) = = sombreros[2] Las cinco operaciones que podemos usar con punteros: véase el texto El empleo de punteros con funciones que trabajan con arrays
static int pupa[2][2] = { {12}, {14, pint = pupa[0];
16};
16} };
4. Suponga la declaración static int reja[30][100]; a. Expresar la dirección de reja[22][56] b. Expresar la dirección de reja[22][0] de dos formas distintas. c. Expresar la dirección de reja[0][0] de tres formas distintas. Respuestas 1. D D O O
L L T T 2. Con ello se consigue que la clase de almacenamiento de ref sea extern por defecto, pudiendo ser inicializada. 3. a. 12 y 121; b. 1.0 y 3.0; c. 10023 y 0 (inicialización automática); d. 12 y 16; e. 12 y 14 (a la primera fila sólo va el 12 debido a las llaves). 353
4. a. &reja[22][56] b. &reja[22][0] y reja[22] c. &reja[0][0] y reja[0] y reja
Ejercicio 1. Modifique el programa de lluvia de forma que realice los cálculos usando punteros, y no subíndices. (A pesar de todo, hay que declarar el array.)
13 Tiras de caracteres y funciones relacionadas En este capítulo encontrará: • Definición de tiras dentro de un programa • Tiras constantes • Arrays de tiras: su inicialización • Arrays y punteros • Especificación explícita de tamaño • Arrays de tiras de caracteres • Punteros y tiras • Entrada de tiras • Preparando espacio • La función gets( ) • La función scanf( ) • Salida de tiras • La función puts( ) • La función print( ) • La opción “hágaselo usted mismo” • Funciones de tiras de caracteres • La función strlen( ) • La función strcat( ) • La función strcm p( ) • La función strcpy( ) • Ejemplo: ordenación de tiras • Argumentos en líneas de ejecución • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios
357
Tiras* de caracteres y funciones relacionadas CONCEPTOS Tiras de caracteres Inicialización de tiras de caracteres E/S de tira Utilización de funciones para tiras Argumentos en líneas de ejecución
/* Autobombo. Hagase en solitario */ #include #define MSJ "Debe tener muchas cualidades. Digame algunas." / * constante simbolica de tira de caracteres * / #define NULL 0 #define LIM 5 #define LONLIN 81 /* longitud maxima + 1 */ char m1[] = "Limitese a una sola linea." /* inicializacion de un array de char externo */ char *m2 = "Si no se le ocurre nada, muerase."; / * inicializacion de un puntero a char externo */ main() { char nombre[LONLIN]; static char talentos[LONLIN]; int i ; int cont = 0; char *m3 = "\nYa basta sobre mi -- como se llama?"; / * inicializacion de un puntero */ static char *mistal[LIM] = { "Sumo numeros con sutileza", "Multiplico con precision", "Almaceno datos", "Sigo instrucciones al pie de la letra", "Entiendo el lenguaje C”}; /* inicializacion de un array de tiras */ printf("Hola! Soy Juanito, su ordenador favorito.\n”); printf("%s\n", "Tengo muchas cualidades. Le dire algunas. "); puts("Las tengo en la punta del byte...Ah, si! ahi van:"); for (i = 0; i < LIM; i++) puts (mistal[i]); /*imprime las cualidades del ordenador*/ puts(m3); gets(nombre);
Las tiras de caracteres (character strings) constituyen uno de los tipos de datos más importantes y útiles del lenguaje C. Aunque venimos usando tiras de caracteres a lo largo de todo el libro, aún nos queda mucho que aprender acerca de ellas. Por supuesto, conocemos ya su característica más básica: una tira de caracteres no es más que un array de tipo char terminado con un ca rácter nulo (‘ \ 0’). En este capítulo aprenderemos nuevos detalles acerca de las tiras, cómo declararlas e inicializarlas, cómo meterlas y sacarlas de un pro grama y, finalmente, cómo manipularlas. En la figura 13.1 se presenta un programa en el que se ejemplarizan va rias formas de preparar tiras, de leerlas y de imprimirlas. Utilizaremos dos nuevas funciones: gets( ), que captura una tira de caracteres, y puts( ), que la imprime. (Probablemente habrá notado un cierto parecido con getchar( ) y putchar( ).) El resto del programa le resultará bastante familiar.
* A lo largo de la obra traduciremos el término character string por “tira de caracteres”. También puede ser traducido por “cadena”.
}
printf("Bien, %s. %s\n", nombre, MSJ) ; printf("%s\n%s\n", m1, m2) ; g e t s (talentos); puts("A ver si he cogido toda la lista:"); puts(talentos); printf("Gracias por la informacion, %s. \n", nombre) ;
Figura 13.1 Programa para usar strings
Para ayudarle a desentrañar los misterios del programa, indicamos a con tinuación su salida: Hola! Soy Juanito, su ordenador favorito. Tengo muchas cualidades. Le dire algunas. Las tengo en la punta del byte. ..Ah, si! ahi van: Sumo numeros con sutileza Multiplico con precision Almaceno datos Sigo instrucciones al pie de la letra Entiendo el lenguaje C Ya basta sobre mi -- Como se llama? Jose Perez 359
Bien, Jose Perez. Debe tener muchas cualidades. Digame algunas. Limitese a una sola linea. Si no se le ocurre nada, muerase. Borracho, mujeriego, programador, catador de queso y soltero. A ver si he cogido toda la lista: Borracho, mujeriego, programador, catador de queso y soltero. Gracias por la informacion, Jose Perez.
Profundicemos ahora en los distintos componentes del programa. No va mos a realizar un análisis línea por línea; por el contrario, realizaremos los comentarios de una forma más organizada. En primer lugar, nos fijaremos en las distintas formas de definir una tira en el programa; a continuación, observaremos las distintas peculiaridades que intervienen en la lectura de la tira; por último, estudiaremos las formas de salida de la tira.
con el nombre de un array, que era, a su vez, un puntero a la localización del array. Si lo anterior es cierto, ¿qué tipo de salida produciría esta línea? /* tiras como punteros */ main()
{ printf("%s, %u, %c\n","Me", "gustas", *"tu") ;
} El formato %s imprimirá la tira, por lo que debemos esperar que aparezca Me. Por su parte, el formato %u produce como salida un entero sin signo. Si la palabra “gustas” se comporta como puntero, este formato debería im primir el valor del puntero “gustas”, es decir, la dirección del primer carác ter de la tira. Por último, *“tu” deberá dar como resultado el valor de la
dirección a la que apunta este puntero, es decir, el primer carácter de la pro pia tira. Pero bueno, ¿realmente es así? Bien, veamos la salida:
Definición de tiras dentro de un programa Habrá observado, mientras revisaba el programa, que existen muchas for mas diferentes de definir una tira. Nos dedicaremos ahora a repasar las más importantes: de tiras constantes, de arrays de tipo char, de punteros a char y de arrays de tiras de caracteres. Además, el programa deberá asegurarse de que dispone de suficiente espacio de almacenamiento para guardar la tira; también nos ocuparemos de eso. Tiras constantes
Dondequiera que el compilador encuentra cualquier cosa circundada por comillas, lo asimila como un tira constante. Los caracteres que estén ence rrados entre las comillas, junto con un carácter ‘ \ 0’, se almacenan en posi ciones adyacentes de memoria. El ordenador cuenta el número de caracteres, de forma que conoce de antemano cuánta memoria va a necesitar para su almacenamiento. Nuestro programa utiliza varias tiras constantes de esta clase, sobre todo como argumentos de las funciones printf( ) y puts( ). Obsérvese, además, que podemos definir una tira constante en preprocesador con #define. Cuando se desea que figure un signo comillas dentro de la tira, se deberá anteponer al mismo un carácter barra-atrás:
Me, 34, t
¡Tatatachán! Dediquémonos ahora a las tiras almacenadas como arrays de tipo char. Arrays de tiras: inicialización Cuando se define un array de tira de caracteres, se debe indicar al compi lador cuánto espacio debe reservar. Una forma de hacerlo es inicializar el array con una tira constante. Como ya se indicó, los arrays auto no pueden inicializarse; por tanto, deberemos utilizar arrays de tipo static o extern para este propósito. Por ejemplo, char m1[] = "Limitese a una sola linea.";
inicializa el array m1 como external (por defecto), asignándole el valor de la tira indicada. Esta forma de inicialización no es más que una manera más corta de expresar la inicialización estándar de arrays:
printf("\"Vuela, vuela, pajarito!\" dijo Juan.\n");
La salida es: "Vuela, vuela, pajarito!” dijo Juan.
Las tiras constantes se guardan en modo de almacenamiento static. La frase entera que se coloca entre comillas actúa como puntero al lugar donde se ha almacenado la misma. La situación es análoga a la que encontrábamos
(Observe el carácter nulo de cierre; sin él lo que tendríamos sería un array de caracteres, no una tira.) De cualquiera de las maneras (recomendamos fer vientemente la primera), el ordenador cuenta los caracteres y prepara un array del tamaño correspondiente. 361
Al igual que en los arrays comentados anteriormente, el nombre del array m1 es un puntero al primer elemento del mismo: m1 == &m1[0]
*m1 == 'L'
*(m1+1) == m1[1] == 'i'
De hecho, se puede utilizar notación de punteros para la preparación de la tira. Por ejemplo, usaríamos char *m3 = "\nBasta por hoy - seguimos otro dia";
que es lo mismo (o casi) que decir static char m3[] = "\nBasta por hoy - seguimos otro dia";
Ambas declaraciones han preparado m3 como un puntero a la tira co rrespondiente. En ambos casos, la propia tira determina la cantidad de al macenamiento que se reserva para ella misma. Sin embargo, las dos formas no son completamente idénticas.
ARRAYS Y PUNTEROS: DIFERENCIAS
En el texto acabamos de discutir las diferencias entre el empleo de una declaración de las dos formas siguientes: static char nena[] = "Quiero a papa!"; char *nene = "Quiero a mama!";
La diferencia más significativa es que el puntero nena es una constante, mientras que nene es una variable. Veamos en qué queda esta diferencia en la práctica. En primer lugar, en ambas formas se puede usar la adición de punteros. for (i = 0; i < 6; i ++) putchar( *(nena + i)); putchar('\n'); for (i = 0; i < 6; i++) putchar( *(nene + i)); putchar('\n');
Arrays y punteros
Pero bueno, ¿cuál es la diferencia? La forma array genera un array de 35 elementos (uno por cada carácter y uno más por el '\0' final) en almacenamiento estático. Cada elemento se inicializa a su carácter correspondiente. A partir de ese momento, el compi lador reconocerá el nombre m3 como sinónimo de la dirección del primer elemento del array, &m3[0]. Un detalle importante a destacar aquí es que m3 es un puntero constante; no se puede cambiar m3 porque implicaría alterar la localización (dirección) en que está almacenado el array. Se pueden utili zar operaciones como m3 + 1 para identificar el siguiente elemento del array, pero no se permite + + m3. Tenga presente que el operador incremento sólo se puede emplear con nombres de variables, no con constantes. El puntero también genera 35 elementos en almacenamiento estático, co locados en forma de tira. Además, prepara una localización de memoria ex tra para la variable puntero m3. Esta variable apunta inicialmente al comien zo de la tira, pero su valor se puede alterar. Así, está permitido en este caso utilizar el operador incremento; de hecho, + + m3 haría que éste apuntase al segundo carácter (B). Observe, además, que no tenemos que declarar *m3 como static. La razón es que no estamos inicializando, en este caso, un array de 35 elementos, sino simplemente una variable de tipo puntero. No existen restricciones para la inicialización de variables ordinarias (es decir, que no son arrays) por lo que concierne a clases de almacenamiento. ¿Son realmente importantes estas diferencias? Frecuentemente, no; pero todo depende de lo que se pretenda hacer. Observe el cuadro siguiente, en el que se ofrecen algunos ejemplos. Entre tanto, volveremos al problema de la creación de espacio de almacenamiento para las tiras de caracteres.
da como salida Quiero Quiero
Por otra parte, únicamente la versión puntero puede utilizar el opera dor incremento while ( *(nene) != '\0') /* acaba al final de la tira */ putchar( *(nene++); /* imprime caracter, avanza puntero */
resulta Quiero a mama!
Supongamos que queremos igualar nena a nene. Se puede escribir nene = nena; /* nene apunta ahora al array nena
*/
pero no está permitido nena = nene; /* construccion ilegal */
La situación es análoga a escribir x = 3; o bien 3 = x;. La parte izquier da de la sentencia de designación debe ser siempre una variable. Por cierto, 363
la igualdad nene = nena; no hace que desaparezca la tira de caracteres de mamá; simplemente cambia la dirección almacenada en nene. Existe una manera de alterar el mensaje de nena, que consiste en intro ducirse en el propio array. nena[7] = 'm' ; nena[9] = 'm' ;
Como nombre es un array pensado para ser leído en ejecución, el compi lador no tiene manera alguna de saber cuánto espacio debe reservar con an telación, a menos que se lo indiquemos explícitamente. Dicho de otra forma, no existe una tira de caracteres presente que el compilador pueda utilizar co mo referencia para cálculo del tamaño de almacenamiento. Así pues, elegi mos un tamaño lo suficientemente grande como para que se pueda introdu cir el nombre del usuario sin problemas (el tamaño escogido equivale a un reglón completo de la pantalla).
o bien Arrays de tiras de caracteres *(nena + 7) = 'm'; *(nena + 9) = 'm';
Los elementos del array son variables; el nombre del mismo no lo es.
Especificación explícita de tamaño
A menudo es conveniente disponer de un array de tiras. De esta forma, se puede utilizar un subíndice para acceder a distintas tiras. También hemos empleado esta configuración en el ejemplo: static char *mistal[LIM] = { "Sumo numeros con sutileza", "Multiplico con precision", "Almaceno datos", "Sigo instrucciones al pie de la letra", "Entiendo el lenguaje C"};
Otra forma de disponer de sitio para almacenar la tira es declararlo direc tamente. En la declaración external podríamos haber dicho char m1[40] = "Limitese a una sola linea.";
en lugar de char m1[] = "Limitese a una sola linea.";
La única precaución a observar es que el número de elementos declara dos sea, como mínimo, uno más (el correspondiente al carácter nulo) de la longitud de la tira. Al igual que en los demás arrays de tipo estático o exter no, cualquier elemento no utilizado será inicializado a 0 (el cual, en formato char, corresponde al carácter nulo, no al carácter 0, como dígito).
Estudiemos esa declaración más detenidamente. Como hemos seleccio nado para LIM el valor 5, podemos afirmar que mistal es una array de cinco punteros a tiras de caracteres. Por supuesto, cada tira es, por su parte, un array de caracteres, por lo que, en resumen, disponemos de cinco punteros a arrays. El primer puntero es mistal[0], y apunta a la primera tira. El segun do es mistal[1], y apunta a la segunda. En concreto, cada puntero apunta al primer carácter de cada tira: *mistal[0] == 'S', *mistal[1] == 'M', *mistal[2] == 'A'
y así sucesivamente. La inicialización sigue las reglas establecidas para arrays; la porción en tre llaves es equivalente a
Los elementos sobrantes se iniclalizan a \0
{{. . . static char mascota [12] = “lindo pez";
Figura 13.2
Inicialización de un array
Observe que en el programa inicial asignamos tamaño al array nombre
},{...},...,{...}};
en la que los puntos indican las distintas entradas, que somos demasiado va gos para escribir explícitamente. El detalle principal que queremos destacar es que el primer conjunto de comillas corresponde a una pareja de llaves, y se usa, por tanto, para inicializar el puntero al primer carácter de la tira. El siguiente conjunto de comillas inicializa el segundo puntero, etc. Entre con juntos vecinos se coloca una coma. También aquí podríamos haber declarado explícitamente el tamaño de las tiras de caracteres utilizando una declaración del tipo
char nombre[81];
static char mistal[LIM] [LONLIN] ; 365
Hay una diferencia en esta segunda forma de declaración, en la que obte nemos un array “rectangular” con todas las filas de la misma longitud. Por el contrario, static char
* m i s t a l l [
/* punteros y tiras */ #define PX(X) printf("X = %s; valor = %u; &X = %u\n", X, X, &X) main()
{ static char *mnsj = "Te estas pasando!"; static char *copia;
LIM];
prepara un array “no uniforme”, en el que cada longitud de fila está deter minada por la tira inicializada. Ese array comentado en último lugar no des perdicia espacio de almacenamiento alguno.
copia = mnsj ; p r intf("%s\n", copia); PX(mnsj) ;
}
PX(copia);
Observando el programa se podría inferir que realiza una copia de la tira “¡Te estás pasando!”. Además, esta observación a bote pronto se vería con firmada, en princpio, por la salida del programa: Te estas pasando! m n sj = Te e stas p asand o!; va lor = 14 ; & m nsj = 32 c o p ia = T e e s t a s p a s a n d o ! ; v a l o r = 1 4 ; & c o p ia = 3 4
Figura 13.3
Pero estudiemos cuidadosamente la salida de PX( ). En primer lugar, X, que ha sido sucesivamente colocada como mnsj y copia, se imprime como tira de caracteres (%s). Hasta aquí no hay sorpresas; las dos tiras son “¡Te estás pasando!”. En segundo lugar...; bueno, volveremos más adelante a este punto. El tercer ítem que se imprime en cada línea es &X, la dirección de X. Los dos punteros mnsj y copia están almacenados en las direcciones 32 y 34, res pectivamente. Fijémonos ahora en el segundo ítem, el que hemos llamado valor. Es el propio X. El valor del puntero es la dirección que contiene. Se puede ver que tanto mnsj como copia apuntan a la dirección de memoria 14. El significado de este último aserto es que la tira en sí misma no ha sido copiada. Lo único que hace la sentencia copia = mnsj; es producir un segun do puntero apuntando a la misma tira. ¿Y para qué todo este lío? ¿Por qué no copiamos la tira completa? Bien, convendrá con nosotros que es mucho más eficiente copiar una dirección que, por ejemplo, 50 elementos distintos. A menudo, lo único que se necesita pa ra realizar esta tarea es la dirección. Ahora que ya hemos discutido la definición de tiras dentro del programa, nos dedicaremos a la lectura de tiras de caracteres.
Arrays no uniformes y rectangulares Punteros y tiras
Quizá habrá observado, a lo largo de nuestra discusión sobre tiras, ciertas referencias ocasionales a los punteros. La mayoría de operaciones que atañen a las tiras de caracteres se efectúan en C con punteros. Por ejemplo, consideremos el siguiente programa, tan inútil como instructivo:
Entrada de tiras La introducción de una tira en un programa tiene dos etapas: prepara ción de espacio para su almacenamiento y empleo de una función de entrada para capturar la tira. 367
/*
getnombrel */
main()
{
Preparando espacio
Lo primero que hay que hacer es disponer de un sitio donde colocar la tira una vez que se haya leído. Tal como se mencionó con anterioridad, esto significa contar con espacio suficiente para meter todas las tiras que se vayan a leer. No espere que el ordenador cuente la longitud de la tira conforme se va leyendo y prepare espacio para ella en ese momento; no funcionará (a me nos que escriba un programa que lo haga así). Si intenta ejecutar algo como esto: static char *nombre; scanf("%s", nombre) ;
se encontrará con que el compilador probablemente “lo traga”; pero en el mismo instante que se lea nombre, se guardará sobre otros datos o parte del código del propio programa. La mayoría de los programadores opinan que una situación así es realmente graciosa, pero únicamente cuando le sucede a los programas ajenos. La forma más simple de preparar espacio es definir explícitamente el ta maño del array en la propia declaración:
char nombre[81]; /* reserva espacio */
}
Esta función aceptará cualquier nombre (incluyendo espacios) de hasta 80 caracteres de largo. (Recuerde que se debe reservar un espacio para ‘ \ 0’.) Observe que deseamos que gets( ) afecte algo (nombre) en el programa de llamada. Así pues, deberemos utilizar un puntero como argumento: por supuesto, el nombre del array es un puntero. De todas formas, la función gets( ) es más sofisticada que lo que sugiere el ejemplo anterior. Observe éste: /* getnombre2 */ main()
{
char nombre[80]; char *ptr, *gets();
char nombre[81];
Otra posibilidad es usar funciones de la librería C que asignan memoria, de las cuales hablaremos en el capítulo 15. En nuestro programa, utilizamos un array de tipo auto para nombre. Lo hicimos así porque no teníamos que inicializarlo. Lina vez dispuesto el espacio de almacenamiento suficiente para la tira, se puede proceder a su lectura. Como ya hemos comentado, las subrutinas de entrada no forman parte de la definición del C. Sin embargo, la mayoría de los sistemas han dispuesto dos funciones de librería para la captura de da tos: scanf( ) y gets( ); ambas pueden utilizarse para leer tiras. De ellas, la más usual es gets( ), que discutiremos en primer lugar. La función gets( )
La función gets( ) (del inglés get string) es muy útil y manejable para pro gramas interactivos. Su actuación consiste en la captura de una tira que se introduce por el periférico de entrada estándar de su sistema, que en adelan te supondremos que es un teclado. Como la tira no tiene una longitud prede terminada, gets( ) necesita una forma de saber cuándo debe acabar de leer. El método empleado es leer caracteres hasta encontrar un carácter “nueva línea” (‘ \ n’), el cual se genera pulsando la tecla [enter]. En ese momento se toman todos los caracteres con excepción del carácter nueva línea, se aña de al final un carácter nulo (‘ \ 0’) y se devuelve la tira al programa de llama da. Seguidamente se presenta un sencillo ejemplo:
printf("Hola, como te llamas?\n"); gets(nombre); /* introduce entrada en tira "nombre" */ printf("Bonito nombre, %s.\n", nombre) ;
}
printf("Hola, como te llamas?\n"); ptr = gets(nombre); printf("%s? Ah! %s!\n", nombre ptr);
Una posible salida sería: Hola, como te llamas? Pedro Piedra Pedro Piedra? Ah! Pedro Piedra!
¡Gets( ) ha ejecutado la entrada de los datos de dos formas diferentes!
1. Utiliza el método del puntero para colocar la tira en nombre. 2. Utiliza la palabra clave return para devolver la tira a ptr. Observe que ptr es un puntero a char. Esto quiere decir que gets( ) debe devolver un valor que es, asimismo, un puntero a char; de ahí la declaración efectuada en la sección correspondiente en el segundo ejemplo. Este formato de declaración char *gets();
indica que gets( ) es una función (por eso lleva paréntesis) del tipo “punteroa-char” (por eso se le coloca un * y un char). En el primer ejemplo pudimos 369
pasar el programa sin esta declaración porque no teníamos intención de utili zar el valor de retorno de gets( ). Por cierto, se puede declarar también una variable como puntero a una función. Tendría un aspecto como éste: char (*fino)();
donde fino sería un puntero a una función de tipo char. Volveremos sobre estas curiosas declaraciones en el capítulo 14. La estructura de la función gets( ) deberá ser algo como:
(espacio, tabulado o nueva línea). Por otra parte, si se especifica una anchura de campo, por ejemplo %10s, la función scanf( ) recoge o bien 10 caracte res, o los caracteres que haya antes del primer espacio en blanco; la elección está determinada por la situación que se dé en primer lugar. Scanf( ) devuelve un valor entero igual al número de ítems leídos, cuan do funciona correctamente, o bien, cuando encuentra un carácter EOF, de vuelve este último. /*
{
static char nombre1[40], nombre2[11] ; int cont;
char *gets(s) char *s;
{
char *p;
}
... return(p) ;
} En realidad, la estructura es un poco más complicada, porque gets( ) tie ne dos retornos posibles. Si todo va bien, devuelve en el return la tira leída, tal como se ha dicho; si hay algo equivocado, o la función encuentra un ca rácter EOF, devuelve una dirección 0 o NULL. Así pues, gets( ) lleva incor porado un cierto control de errores. Esta disposición hace conveniente el uso de sentencias como:
scanf() contando */
main()
printf("Introduzca dos nombres.\n"); cont = scanf("%s %10s", nombre1 , nombre2) ; printf ("He leido los %d nombres %s y %s. \n", cont, nombre1, nombre2);
Veamos dos ejemplos de salidas: Introduzca dos nombres. Jacinto Juan He leido los 2 nombres Jacinto y Juan. Introduzca dos nombres. Jose Papapopoulos He leido los 2 nombres Jose y Papapopoul.
while (gets(nombre) != NULL)
en la que NULL está definido en stdio.h como 0. La faceta puntero de nues tra reciente adquisición asigna un valor a nombre. La faceta return, por su parte, asigna un valor a gets(nombre), tomado como un todo, y permite com probar la aparición de un EOF. Esta disposición de dos caras de la función gets( ) es más compacta que la permitida por getchar( ), la cual devolvía un valor en return, pero carecía de argumento. while ((ch = getchar())
!= EOF)
La función scanf( ) Ya hemos empleado con anterioridad scanf( ) con el formato %s para leer una tira de caracteres. La mayor diferencia entre scanf( ) y gets( ) está en la toma de decisión del final de la tira; scanf( ), más que una función para capturar tiras, deberíamos llamarla una función “capturapalabras”. Como ya hemos visto, la función gets( ) captura todos los caracteres hasta encon trar un carácter nueva línea. La función scanf( ) puede terminar la lectura de dos maneras distintas. En cualquiera de las dos, la lectura comienza con el primer carácter no blanco que se encuentra. Si se está usando el formato %s, la tira continúa hasta (exclusive) el siguiente espacio en blanco localizado
En el segundo ejemplo se han leído únicamente los 10 primeros caracte res de Papapopoulos, por estar utilizando el formato %10s. Cuando se trata de leer esto desde teclado, es aconsejable emplear gets( ). Es más fácil de utilizar, más rápida y más compacta. El uso principal de scanf( ) será en aquellos casos en que debamos introducir una mezcla de da tos de tipo diferente de alguna manera estándar. Por ejemplo, si cada línea de entrada contiene el nombre de una herramienta, su número de almacén y su precio, deberá emplear la función scanf( ). O mejor aún, escribir por su cuenta una función “a medida” que lleve incluido algún control de erro res. Pasaremos ahora a discutir el proceso de salida de tiras.
Salida de tiras De nuevo en este caso nos apoyaremos en funciones de librería, por lo que pueden aparecer ligeras diferencias de un sistema a otro. Las dos funcio nes fundamentales en salida de tiras de caracteres son puts( ) y printf( ). 371
La función puts( )
Es ésta una función muy fácil de utilizar; únicamente necesita un argu mento que sea un puntero a una tira de caracteres. En el siguiente ejemplo se muestran algunas de las muchas formas de hacerlo: /* puts facilon */ #include #define "Naci de un #define." main(){ static char str1[] = "Un array me inicializo."; static char *str2[] = "Un puntero me inicializo.";
}
puts("Soy un argumento de puts().") ; puts(DEF); puts(str1) ; puts(str2) ; puts(&str1[4]) ; puts(str2+4);
Observe, también, que cada tira impresa por la función puts( ) aparece en una línea diferente. Lo que sucede es que cuando puts( ) encuentra el ca rácter nulo final, lo sustituye por un carácter nueva línea y lo envía junto con la tira. La función printf( )
Ya hemos discutido printf( ) con bastante minuciosidad anteriormente. Al igual que puts( ), toma un puntero a una tira como argumento. La fun ción printf( ), sin embargo, resulta ligeramente menos adecuada que puts( ), pero es más versátil. Una diferencia entre ambas funciones es que printf( ) no añade el carác ter nueva línea tras la tira automáticamente. Por ello, deberemos indicarle dónde deseamos que salte la línea. Así printf("%s\", tira);
tiene el mismo efecto que
La salida sería:
puts(tira) ;
Soy un argumento de puts(). Naci de un #define. Un array me inicializo. Un puntero me inicializo. rray me inicializo. untero me inicializo.
Este ejemplo nos recuerda que las frases entre comillas y los nombres de las tiras de caracteres son punteros. Fíjese, además, en los dos ejemplos fina les. El puntero &str1[4] apunta al quinto elemento del array str1. Dicho ele mento contiene el carácter “r”, que es el utilizado por puts( ) como punto de partida. De igual forma, str2 + 4 apunta a la célula de memoria que con tiene la “u” de “puntero”, que es donde comienza la salida. ¿Cómo sabe puts( ) dónde debe terminar? La función termina allá don de encuentre el primer carácter nulo. Por la cuenta que le trae, procure que haya uno. ¡¡Nunca haga esto!!
Como habrá observado, la primera forma requiere más trabajo de tecla. También consume mayor tiempo de ordenador en ejecución. Por otra parte, printf( ) hace más simple la tarea de combinar tiras en una sola línea de es critura. Por ejemplo, printf("Bien, %s, %s\n", nombre, MSJ);
combina “Bien” con el nombre del usuario y una tira de caracteres, todos ellos en la misma línea.
La opción "hágaselo usted mismo"
/* Noooooo !!!!! */ main()
{ static char asino[] = {'H', 'O', 'L', 'A' };
}
puts(asino);
/* no es una tira de caracteres */
Al no disponer asino de un carácter nulo como terminación, no se consi dera una tira de caracteres; además, al faltar el carácter nulo, puts( ) no sa brá dónde ha de terminar; por tanto, continuará viajando por las direcciones de memoria, a partir de la dirección donde se encuentre asino, hasta encon trar un carácter nulo. Si tiene suerte, lo encontrará en la siguiente célula de memoria; recuerde, sin embargo, que no siempre va a ser tan afortunado.
No tenemos por qué estar limitados por estas opciones de biblioteca en entrada/salida. Si no las posee su sistema, o simplemente no le gustan, se puede preparar sus propias versiones a partir de getchar( ) y putchar( ). Supongamos que le falta la función puts( ). El programa siguiente es un ejemplo de una posible implementación: /* put1 -- imprime una tira */ put1(tira) char *tira; { while(*,tira != '\0') putchar(*tira++); putchar('\n');
} 373
El puntero char tira apunta inicialmente al primer elemento del argumen to de llamada. Una vez impreso el contenido de tal elemento, el puntero se incrementa y apunta al elemento siguiente. Este proceso continúa hasta que el puntero se encuentra apuntando a un elemento que contiene el carácter nulo. En ese momento se detiene la impresión y se envía un carácter nueva línea. Supongamos ahora que disponemos de una función puts( ), pero desea mos otra que indique, además, el número de caracteres que se han impreso. Es muy fácil añadir este detalle:
en primer lugar, ejecutarla, produciendo la tira que se ha de imprimir. La salida es la siguiente: Si tuviera dinero suficiente, arreglaria de una vez esa gotera. He contado 33 caracteres.
A estas alturas debe ser capaz de construir una versión de gets( ) que fun cione adecuadamente; deberá ser semejante, aunque mucho más sencilla, que la función getint( ) que presentamos en el capítulo 10.
/* put2 -- imprime una tira y cuenta sus caracteres */ put2(tira) char *tira;
Funciones de tiras de caracteres
{
int cont = 0; while(*tira != '\0')
{
putchar(*tira++); cont++;
}
}
putchar('\n'); return(cont) ;
La llamada put2("pizza") ;
imprimiría la tira de caracteres pizza, mientras que la llamada
La mayoría de bibliotecas C presentan funciones para manejo de tiras de caracteres. Estudiaremos ahora las cuatro funciones más útiles y más comu nes: strlen( ), strcat( ), strcmp( ) y strcpy( ). Ya hemos utilizado strlen( ), que es la encargada de calcular la longitud de una tira. La usaremos en el próximo ejemplo, en una función dedicada a cortar tiras demasiado largas. La función strlen( ) /* función de censura */ corta(tira, largo) char *tira; int largo;
num = put2("pizza");
enviaría también un entero que simbolizase el número de caracteres impresos a la variable num; en este caso, num tomaría el valor 5. He aquí una versión ligeramente más elaborada que utiliza funciones anidadas: /* funciones anidadas */ #include mai n()
}
if ( strlen(tira) > largo) *(tira + largo) = '\0';
Pruébela con este programa: /* test */
main()
{
}
put1("Si tuviera dinero suficiente,\n"); printf("He contado %d caracteres.\n", put2("arreglaria de una vez esa gotera.") );
static char mens[] = "Agarreseme que vienen curvas.";
} (La razón del #include < stdio.h > es que en nuestro sistema putchar( ) está definido allí, y estas funciones lo utilizan.) ¡Vaya! estamos empleando printf( ) para imprimir el valor de put2( ), pero en la propia acción de búsqueda del valor de put2( ) el ordenador debe,
puts(mens); corta(mens,10); puts(mens);
La salida debe ser: Agarresele que vienen curvas. Agarreseme
375
Nuestra función coloca un carácter ‘ \ 0’ en el elemento undécimo del array, que corresponde a un blanco. El resto del array continúa allí, pero putsf() se detendrá en el primer carácter nulo, ignorando la porción restante. La función strcat( )
He aquí un ejemplo de lo que strcat( ) es capaz de hacer:
Hemos añadido 1 a las longitudes combinadas con el fin de disponer de espacio para el carácter nulo. La función strcmp( )
Supongamos que desea comparar una respuesta tecleada al ordenador con una tira almacenada previamente: /* Funciona esto? */ #include #define RESP "Blanco" main() { char prueba[40];
/* uniendo dos ti ras */ #tinclude main() { static char flor[80]; static char apendice[] = "huelen a zapato usado.";
puts("De que color es el caballo blanco de Santiago?"); gets(prueba); while (prueba != RESP)
puts("Cuales son sus flores favoritas?"); gets(flor);
{
strcat(flor, apendice); puts(f1or); puts(apendice);
puts("Ni idea. Prueba otra vez."); gets(prueba);
} puts("Correcto!");
} La salida es: Cuales son sus flores favoritas? las rosas las rosas huelen a zapato usado, huelen a zapato usado.
Como puede observar, strcat( ) (del inglés string concatenation) toma dos tiras de caracteres como argumento, añade una copia de la segunda al final de la primera y hace que esta versión combinada sea la nueva tira primera. La segunda tira de caracteres no se altera. ¡Precaución! Esta función no comprueba si la segunda tira dispone de espacio suficiente para almacenarse tras la primera. Si no toma precauciones al respecto, se puede encontrar con muchos problemas. Evidentemente, po demos emplear strlen( ) para asegurarnos antes de saltar al vacío.
/* Este si funciona */ #include #define RESP "Blanco" main() { char prueba[40];
/* unión de dos tiras comprobando si caben */ #include
#define MAX 80 main()
{ static char flor[MAX]; static char apendice[] = " huelen a zapato usado.";
}
puts("Cuales son sus flores favoritas?"); gets(flor); if ((strlen(apendice) + strlen(flor) + 1) strcat(flor, apendice); puts(flor);
A primera vista parece elegante y funcional, pero va a mostrar un fallo en ejecución. Lo que sucede es que prueba y RESP son, en realidad, punte ros; por lo cual, la comparación prueba != RESP no pregunta si las dos ti ras son iguales, sino si las direcciones apuntadas por prueba y RESP son la misma. Como estas dos tiras se almacenan en lugares diferentes, los dos pun teros no podrán coincidir jamás, y el/la usuario/a recibirá siempre un men saje de respuesta equivocada. Programas como éste han llevado al suicidio en más de una ocasión. Lo que necesitamos es una función que compare el contenido de las tiras, no sus direcciones. Podríamos prepararnos una por nuestra cuenta, pero nor malmente encontraremos el trabajo ya hecho con stremp( ) (del inglés string comparison). Podemos ahora arreglar nuestro programa:
puts("De que color es el caballo blanco de Santiago?"); gets(prueba); while (strcmp(prueba, RESP) != 0)
{
< MAX)
puts("Ni idea. Prueba otra vez."); gets(prueba);
}
puts("Correcto! " ); } 377
Al ser considerados como “verdad” los valores distintos de 0, podríamos también abreviar la sentencia while dejándola en while ( strcmp(prueba, RESP)). Se puede deducir de este ejemplo que strcmp( ) toma dos punteros de ti ras como argumentos y devuelve el valor 0 si las tiras son iguales. Si ya había llegado a esa conclusión por su cuenta, anótese un tanto en su marcardor particular. Uno de los detalles más agradables de strcmp( ) es que compara tiras de ca racteres, no arrays. Así, aunque el array prueba ocupa 40 posiciones de memoria, y “Blanco” únicamente 7 (no olvide el carácter nulo), la compara ción se establece únicamente en la parte de prueba que comprende desde el principio hasta su primer carácter nulo. Así pues, strcmp( ) puede emplearse para comparar tiras almacenadas en arrays de diferente tamaño. ¿Qué sucede si el usuario contesta “BLANCO”, o “blanco”, o “blan quísimo”? Bueno, el ordenador responderá que la respuesta es incorrecta. Si se desea hacer un programa más “humanizado”, deberá prever la posibi lidad de respuestas alternativas. Existen algunos trucos en este sentido; por ejemplo, se puede usar un #define con la respuesta como “BLANCO” y es cribir una función que convierta cualquier valor de entrada en mayúsculas únicamente; con ello se eliminaría el problema surgido entre combinaciones de mayúsculas y minúsculas, pero aún quedarían otros cabos por atar. Por cierto, ¿qué valor devuelve strcmp( ) si las tiras son diferentes? Vea mos un ejemplo: /* return de strcmp */ #include main() { printf("%d\n", strcmp("A", "A")), "A") ); printf("%d\n", strcmp("A", "B") ); printf("%d\n", strcmp("B", "A") ); printf("%d\n", strcmp("C", "A") ); printf("%\n", strcmp("manzanas", "manzana") );
primera tira precede a la segunda desde un punto de vista alfabético, en tan to que da valores positivos cuando el orden alfabético es correcto. Además, si comparamos “C” con “A” obtenemos un 2 en lugar de un 1. El compor tamiento parece claro: la función devuelve la diferencia en código ASCII en tre los dos caracteres. Generalizando, strcmp( ) se mueve por la tira hasta que encuentra el primer par de caracteres diferentes; entonces devuelve la di ferencia ASCII de los mismos. Por ejemplo, en el último listado manzanas y manzana coinciden en todos los caracteres excepto en el último, la “s” fi nal de la primera tira. Dicho carácter está emparejado a efectos de compara ción con el noveno carácter de manzana, que corresponde a su carácter nulo, ASCII 0. Por tanto, el valor devuelto es 's' - ' \ 0 ' = 115 — 0 = 115 en que 115 es el código ASCII de “s”. Generalmente, uno no se preocupa del valor exacto que se ha devuelto. La forma más usual de empleo de strcmp( ) implica conocer exclusivamente si el valor de retorno es o no 0; es decir, si las tiras son iguales o diferentes; tampoco nos preocupa el valor concreto cuando se trata de ordenar tiras al fabéticamente; en ese caso nos preocuparía exclusivamente si el valor devuel to es positivo, negativo o 0. Se puede utilizar esta función para comprobar si un programa debe dete ner la entrada de datos: /* comienzo de un programa */
#include #define TAM 81 #define LIM 100 «define STOP " " /* una tira nula */
main() { static char entra[LIM][TAM]; int ct = O;
}
while (gets(entra[ct]) != NULL && strcmp(entra[ct], STOP) != 0 && ct++ < LIM)
La salida es: }
Tal como prometimos, la comparación de “A” consigo mismo devuelve un 0. Si comparamos “A” con “B” obtenemos un -1, mientras que la mis ma comparación en orden inverso da como resultado 1. Este peculiar com portamiento sugiere que strcmp( ) devuelve un número negativo cuando la
Este programa abandona la lectura de la entrada cuando encuentra un carácter EOF (gets( ) devuelve un NULL (nulo) en este caso), o bien cuando se pulsa la tecla [enter] al comienzo de una línea (es decir, se introduce una tira vacía), o también cuando se alcanza el límite LIM. Una entrada de tira vacía permite al usuario una forma muy cómoda de acabar la introducción de una frase. Pasemos ahora a la última función de manejo de tiras que discutiremos en este capítulo. 379
La función strcpy( )
Ya hemos dicho que si pts1 y pts2 son punteros a tiras de caracteres, la expresión pts2 = ptsl ;
En resumen, strcpy( ) utiliza dos punteros a tiras como argumentos. El segundo puntero, que apunta a la tira original, puede ser un puntero declara do, un nombre de array, o una tira constante; sin embargo, el primer punte ro, que apunta a la copia, deberá necesariamente apuntar a un array, o por ción del mismo, de suficiente tamaño como para guardar la tira a copiar. Una vez revisadas algunas funciones para manejo de tiras, nos dedicare mos a estudiar un programa completo que maneja tiras de caracteres.
copia únicamente la dirección de la tira, no la propia tira. Supongamos, sin embargo, que se desea copiar una tira. Para ello puede utilizar la función strcpy( ). Funciona como sigue: /* strcpy() en accion */ #include #define FRASE "Reconsidere su ultima entrada, por favor." main()
{ static char *orig = FRASE; static char copia[40];
puts ( o r i g )
;
puts(copia); strcpy(copia, orig); puts(orig); puts(copia);
}
Ejemplo: ordenación de tiras Abordaremos un problema práctico bastante común: una ordenación de tiras por orden alfabético. Esta subtarea se puede presentar cuando se prepa ran listas de nombres, se construye un índice y en otras muchas situaciones diarias. Una de las herramientas principales de tal programa deberá ser nece sariamente strcmp( ), que utilizaremos para determinar el orden de dos tiras concretas. El planteamiento general de nuestro programa incluirá: lectura de un array de tiras, ordenación de las mismas e impresión del resultado orde nado. Hace un momento presentábamos un esquema para leer tiras, el cual utilizaremos para comenzar el programa. La parte de salida no tiene mayor problema; respecto a la ordenación, emplearemos el mismo algoritmo que usábamos para números; entretanto, introduciremos en el programa un pe queño truco: obsérvelo con atención y vea si es capaz de descubrirlo.
La salida es: Reconsidere su ultima entrada, por favor. Reconsidere su ultima entrada, por favor. Reconsidere su ultima entrada, por favor.
/* lectura y clasificación de tiras */ #include #define TAM 81 /* limite longitud tira contando \0 #define LIM 20 /* numero maximo de tiras a leer #define PARA "" /* tira nula para detener entrada main()
*/ */ */
{
Podemos observar que la tira apuntada por el segundo argumento (orig) se ha copiado en el array apuntado por el primer elemento (copia). Puede recordar el orden en que se deben introducir los argumentos observando que es el mismo orden usado en las sentencias de asignación: la tira que va a ad quirir el valor está a la izquierda. (La línea en blanco que aparece después de la primera impresión de copia refleja el hecho de que los arrays de tipo static se inicializan a 0, que son caracteres nulos en modo char.) En esta función no es incumbencia del ordenador la preparación de espa cio para la copia en el array de destino; este detalle queda bajo su completa responsabilidad. Esta es la razón por la que hemos empleado la declaración static char copia[40];
y no static char *copia; /* no asigna espacio para la tira */
static char entra[LIM][TAM] ; /* array para entrada */ char *ptira[LIM] ; /* array de variables puntero */ int ct =0; /* contador de entrada */ int k; /* contador de salida */ printf ("Introduzca hasta %d lineas y las ordenare. \n", LIM); printf("Para acabar, pulse [enter] al comienzo de linea. \n") ; while (gets(entra[ct]) != NULL && strcmp(entra[ct], PARA) != 0 && ct++ < LIM) ptira[ct-1] = entra[ct-1]; /* apunta al array sin ordenar */ ordenatira(ptira, ct); /* clasificador tiras */ puts("\nAhi va la lista ordenada:\n"); for (k=0; k < ct; k++) puts(ptira[k]); } /* tiras ordenadas */ /* clasificador de tiras */ ordenatira(tiras, num) char *tiras[]; int num; { 381
char *temp; int tope, busca; f o r ( t o p e = 0 ; tope < num - 1; tope++) for (busca = tope + 1; busca < num; busca++) if (strcmp(tiras[tope] , tiras[busca] ) > 0)
{ temp = tiras[tope]; tiras[tope] = tiras[busca]; tiras[busca] = temp; } } F igu ra 13.4
Programa para lectura y ordenación de tiras
Para probar nuestro programa, usaremos unos versos bastante ripiosos. Introduzca hasta 20 lineas y las ordenare. P a r a a c a b a r , p u l s e [ e n t e r ] a l comienzo de Y tambien en el trabajo Los hay que pasan por caja D ejan do so lo m iga ja P orq ue se lle van e l gajo
linea.
A hi va la lis ta ordena da: D ejan do so lo m iga ja Los hay que pasan por caja P orq ue se lle van e l gajo Y tambien en el trabajo
Bien, parece que la ordenación no ha empeorado ni mejorado la calidad de la rima. El truco que mencionábamos antes se refiere al uso de punteros; en lugar de reordenar las tiras de caracteres, hemos cambiado la ordenación de los punteros a esas tiras. Explicación: al principio, ptira[0] apunta a entra[0], y así sucesivamente. Cada entra[] es un array de 81 elementos, y cada ptira[] es una simple variable. El procedimiento de ordenación reordena ptira, de jando entra tal como está; si, por ejemplo, entra[1] debe aparecer antes que entra[0], alfabéticamente hablando, el programa conmuta su ptira, haciendo que ptira[0] apunte a entra[l] y ptira[1] a entra[0]. Este sistema es más senci llo que usar, digamos, strcpy( ) para intercambiar los contenidos de las dos tiras entra. Vuelva a repasar la figura e intente seguir el proceso completo. Como colofón de este capítulo llenaremos un antiguo vacío de nuestras vidas, concretamente el contenido de los paréntesis de main( ).
Argumentos en líneas de ejecución ¡Atención! Una línea de ejecución en la línea que se teclea para ejecutar su programa (así de fácil). Hasta ahora no hemos entrado en ello. Suponga
mos que tenemos un programa en un fichero llamado rifa. La línea de ejecu ción tendrá un aspecto: % rifa
o quizá A> rifa
por nombrar dos sistemas comunes. Los argumentos en línea de ejecución son ítems adicionales incluidos en la misma línea: % rifa -r Ton ic a
Un precioso detalle de la programación en C es la posibilidad de leer es tos ítems y emplearlos dentro del programa. El mecanismo es utilizar argu mentos en main( ). Un ejemplo típico sería: 383
/* main() con argumentos */ main(argc, argv) int argc; char *argv[];
{
int cont;
}
for (cont = 1; cont < argc; cont++) printf("%s ", argv[cont] ) ; printf("\n");
Coloque este programa en un fichero ejecutable llamado eco y observe lo que sucede: A> eco Puedo ser de una gran ayuda. Puedo ser de una gran ayuda.
mento de tipo int se suele llamar argc (del inglés argument count). El sistema utiliza los espacios en blanco para comprobar dónde acaba una tira y comienza la siguiente. Por tanto, nuestro ejemplo eco tiene seis tiras de caracteres, mien tras que el ejemplo anterior, rifa, tenía únicamente dos. El segundo argu mento de main( ) es un array de punteros a tira. Cada tira de la línea de eje cución queda asignada a su propio puntero. Por convención, se llama a este array de puntero argv (del inglés argument values). En aquellos sistemas que es posible (algunos operativos no lo permiten), argv[0] queda asignado al pro pio nombre del programa. Por tanto, argv[l] se asigna a la siguiente tira, etc. En nuestro ejemplo, tendremos: argv[0] argv[1] argv[2] argv[6]
apunta a apunta a apunta a apunta a
eco Puedo ser ayuda.
(en la mayoria de sistemas)
Su situación más probable en este momento será que ya comprende por qué hemos llamado eco al programa, pero no se explica cómo funciona. Qui zá el próximo párrafo le sirva de ayuda (así lo esperamos).
Figura 13.6
Argumentos en línea de ejecución
Una vez hechas las presentaciones, ya puede identificar las variables que utilizamos; el resto del programa debe ser muy fácil de seguir. Muchos programadores emplean una declaración diferente para argv: main(argc, argv) int argc; char **argv;
Los compiladores C permiten a main( ) tener dos argumentos. El primer argumento representa el número de tiras de caracteres que van a continua ción de la palabra de comando. Por tradición (no por necesidad), este argu
La declaración de argv es realmente equivalente a char *argv[];. Se puede entender también diciendo que argv es un puntero a un puntero a char. Co mo veremos con nuestro ejemplo, el resultado es el mismo; tenemos un array con siete elementos; el nombre del array es un puntero al primer elemento; por tanto, argv apunta a argv[0], y argv[0] es un puntero a char; por consi385
guiente, incluso en la definición original, argv es un puntero a un puntero a char. Se puede usar cualquiera de las formas, aunque pensamos que la pri mera es más clara en su significado. Un uso muy común de los argumentos en línea de ejecución es la indica ción de opciones dentro del programa. Por ejemplo, se pretende utilizar la combinación —r para indicar a un programa de ordenación que desea el or den inverso. Tradicionalmente, las opciones se suelen indicar usando un guión y una letra, como —r. Estos “flags” no significan nada en C; de hecho, de berá usted incluir su propia programación para reconocerlos. Presentamos seguidamente un ejemplo muy modesto demostrando cómo puede un programa comprobar un “flag” y hacer uso de él. /* un comienzo modesto */ #define SI 1 #define NO 0 m a in (argc, argv) int aragc; char *argv[]; {
Hasta ahora hemos aprendido Cómo declarar una tira de caracteres: static, char, fun[ ], etc. Cómo inicializar una tira de caracteres: static char *po = “0!” Cómo se emplea gets( ) y puts( ) Cómo se emplea strlen( ), strcmp( ), strcpy( ) y strcat( ) Cómo se pueden usar argumentos en línea de ejecución Que char *lisa y char lisa[ ] son parecidos, pero diferentes Cómo crear una tira constante: “usando comillas”
Cuestiones y respuestas Cuestiones
1. ¿Qué tipo de error se ha cometido en este intento de declaración de una tira de caracteres? main() { char nombre[] = -{’F’, ’ r’, ’i’, ’o’};
float array[100]; int n; int marca = NO; if (argv[1][0] == '-' && argv[1][1] == 'r') marca = SI;
2. ¿Cuál sería la salida de este programa? #ti nclude main()
. . . . . if marca = NO ordena1(array,n);
{ static char not a[] = "Nos veremos en el examen. "; char *ptr;
else
ordena2(array, n);
. . . . .
} En este programa se comprueba si la primera tira de caracteres escrita tras la orden de ejecución comienza con un guión. A continuación se observa si el siguiente carácter corresponde a un carácter de código r. Si es así, se coloca un “flag”, el cual, a su vez, hace que se acceda a una subrutina de ordena ción diferente. Cualquier tira de caracteres tras la primera es ignorada (ya dijimos que era un programa modesto). Si se está usando el sistema UNIX, probablemente se habrá observado una gran variedad de opciones en línea de ejecución y de argumentos que se ofrecen en este sistema operativo. Todos ellos son ejemplos de argumen tos C en línea de ejecución, ya que la mayoría del propio UNIX está escrito en C. Los argumentos en línea de ejecución pueden también ser nombres de fi cheros, utilizables para dirigir allí las entradas o salidas de su programa. Le mostraremos cómo hacerlo en el capítulo 15.
}
ptr = nota; puts(ptr); puts(++pt r); nota[7] = ’\0’ ; puts(nota) ; puts(++pt r);
3. ¿Qué imprimiría este programa? main()
{
static char comer[] = "Pipas"; char *ptr; ptr = comer + strlen(comer); while (-- ptr >= comer)
}
puts(ptr);
387
4. ¿Cuál sería la salida de este programa? main() {
static char valle[30] = "ablan pero no es"; static char cita [40] = "No se de que h"; char *inclan ="toy de acuerdo."; strcat(valle, inclan); strcat(cita, valle); puts(cita);
} 5. Diseñe una función que tome un puntero a tira como argumento y devuelva un puntero al primer blanco que encuentre en dicha tira. Deberá devolver un punte ro NULL si no encuentra ningún blanco. Respuestas
1. Se debe utilizar un modo de almacenamiento extern o static; dentro de la inicialización se debe incluir un ‘ \0’. 2. Nos veremos en el examen, os veremos en el examen. Nos ve s ve 3.
s
as pas ipas Pipas
4. No
se de que hablan pero no estoy de acuerdo. 5. char *pblanco(tira) char *tira;
{
while (*tira != ' ' && *tira != '\0') tira++; /* se detiene al primer blanco o nulo */ if (*tira == '\0') return(NULL); /* NULL = 0 */ else return(tira) ;
}
Ejercicios 1. Diseñe una función que capture los n caracteres siguientes de la entrada, inclu yendo blancos, tabulados y caracteres nueva línea. 2. Modifique esta función de manera que se detenga tras n caracteres o tras el pri mer blanco, tabulado o nueva línea que aparezca, lo que suceda primero. (No use simplemente scanf( ).) 3. Diseñe una función que capture la siguiente palabra de la entrada; defina una pa labra como secuencia de caracteres sin blancos, tabulados o caracteres nueva lí nea. 4. Diseñe una función que explore la tira especificada hasta la primera aparición de un carácter determinado. La función tendrá que devolver un puntero a dicho ca rácter suponiendo que lo haya encontrado, y un NULL cuando el carácter desea do no se encuentra en la tira.
14 Estructuras de datos y otras lindezas En este capítulo encontrará: • Problema ejemplo: creación de un inventario de libros • Puesta a punto del patrón de la estructura • Definición de variables de estructura • Inicialización de una estructura • Cómo acceder a miembros de la estructura • Arrays de estructuras • Declaración de un array de estructura • Identificación de los miembros de un array de estructuras • Detalles del programa • Estructuras anidadas • Punteros a estructuras • Declaración e inicialización de un puntero estructura • Acceso a los miembros de la estructura por puntero • Cómo enseñar estructuras a las funciones • Utilización de miembros de la estructura • Utilización de la dirección de la estructura • Utilización de un array • Y después de las estructuras, ¿qué? • Un vistazo rápido a las uniones • Otro vistazo a typedef • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios
391
Estructuras de datos y otras lindezas CONCEPTOS Estructuras de datos Patrones, etiquetas y variables de estructuras Acceso a las distintas partes de la estructura Punteros a estructuras Arrays de estructuras Funciones y estructuras Uniones Creación de nuevos tipos
PALABRAS CLAVE struct, union, typedef
OPERADORES —
>
tructuras le parecerán un viejo amigo. Estudiemos un ejemplo completo pa ra comprobar la razón por la que una estructura puede ser necesaria, y cómo crearla y utilizarla.
problema ejemplo: creación de un inventario de libros La señorita Fina Buenaletra desea imprimir un inventario de sus libros. Existe una cierta cantidad de información que le gustaría colocar en cada li bro: su título, su autor, su editorial, su fecha de copyright, el número de páginas, el número de copias y el precio; algunos de estos datos, como los títulos, podrían almacenarse en un array de tiras de caracteres; otros necesi tarían un array de tipo int o de tipo float. Si se construyen siete arrays dife rentes, seguir la pista de cada uno de ellos puede llegar a ser bastante moles to, especialmente si, como la señorita Buenaletra desea, se deben sacar listas completas clasificadas por título, autor, precio, etc. Una solución mucho más elegante sería el uso de un único array, en el que cada elemento contu viese toda la información del mismo libro. Pero, ¿qué formato de datos puede contener a la vez tiras y números y, de alguna forma, mantener la información separada? La respuesta, por su puesto, es el protagonista de este capítulo: la estructura. Para ver cómo se prepara y funciona una estructura, comenzaremos con un ejemplo bastante limitado; con el fin de simplificar el problema, impondremos dos restriccio nes: la primera, indicar únicamente título, autor y precio; la segunda, limitar el inventario a un solo libro. Si tiene más de un libro, no se preocupe; ya mostraremos después cómo extender el programa. Observaremos primero el listado del programa y su salida; luego estudia remos por separado cada uno de sus puntos principales. /* inventario de un solo libro */ #include #define MAXTIT 41 /* longitud maxima del titulo + 1 */ #define MAXAUT 31 /* longitud maxima del autor +1 */ struct biblio { /* nuestro primer patron de estructura: la etiqueta es biblio*/ char titulo[MAXTIT]; /* tira de caracteres para titulo */ char autor[MAXAUT]; /* tira de caracteres para autor */ float precio; /* variable para precio del libro */ }; /* fin del patron de estructura */ main()
A menudo, el éxito de un programa depende de un paso previo muy im portante: encontrar una forma adecuada de representar los datos con los que el programa ha de trabajar. El lenguaje C es muy afortunado en este sentido (y no por casualidad), ya que posee un medio muy potente de representar datos complejos. Este formato de datos, llamado “estructura”, no solamen te es lo suficientemente flexible en su forma básica como para representar multitud de datos distintos, sino que, además, permite al usuario inventar nuevos formatos. Si está familiarizado con los records del PASCAL, las es
{ struct biblio libro; /* declara libro de tipo biblio */ printf("Introduzca titulo del libro.\n"); gets(libro.titulo); /* accede a la porcion titulo */ printf("Introduzca ahora el autor.\n"); gets(libro.autor); printf("Ahora ponga el precio.\n"); scanf("%f", &libro.valor); printf("%s por %s: %.2f pts.\n", libro.titulo, libro.autor, libro.valor); printf("%s: \"%s\" \(%.2f pts.\)\n", libro.autor, libro.titulo, libro.valor) ; } 393
Un ejemplo de salida: Introduzca titulo del libro. La Cria del Cangrejo Malayo Introduzca ahora el autor. Federico Pastichet Ahora ponga el precio. 527.50 La Cria del Cangrejo Malayo por Federico Pastichet: 527.50 pts. Federico Pastichet: "La Cria del Cangrejo Malayo" (527.50 pts.)
La estructura que hemos creado tiene tres partes; en una se almacena el título; en la segunda, el autor, y en la última, el precio. Hay también tres detalles importantes a estudiar: 1. Cómo preparar un formato o “patrón” (template) para la estructura. 2. Cómo declarar una variable que se ajuste a dicho patrón. 3. Cómo acceder a los distintos componentes individuales de una varia ble de tipo estructura.
Puesta a punto del patrón de la estructura El patrón de la estructura es el plano maestro que describe, por así decir lo, la estructura de la estructura. Nuestro patrón tiene el siguiente aspecto: struct biblio { char titulo [MAXTIT]; char autor[MAXAUT]; float precio;
};
Este patrón describe una estructura formada por dos arrays de caracteres y una variable de tipo float. Pasemos a los detalles: En primer lugar aparece la palabra clave struct; esta palabra identifica a lo que viene a continuación como una estructura. A continuación viene una “etiqueta” o “rótulo” (tag) opcional; en este caso la palabra biblio. Este nom bre, biblio, es una etiqueta que nos permitirá referirnos posteriormente a es ta estructura de una manera abreviada. Así, podemos presentar posterior mente la declaración
yendo otras estructuras. Por último, tenemos un punto y coma que cierra la definición del patrón. Hemos colocado este patrón fuera de las funciones (externamente); tam bién podríamos haberlo definido en el interior de una función. En este se gundo caso, el patrón puede utilizarse únicamente dentro de dicha función. Cuando es externa, sin embargo, el patrón queda disponible a todas las fun ciones que haya a continuación de la definición en el programa; por ejem plo, en una segunda función se podría haber definido struct biblio cervantes;
y tal función dispondría de una variable cervantes que tendría el mismo for mato que el patrón enunciado. Ya hemos dicho antes que el nombre-etiqueta es opcional, pero se ha de emplear obligatoriamente cuando se preparan estructuras como la que he mos puesto en el ejemplo, con el patrón definido en un lugar y las variables en otro. Volveremos de nuevo a este punto; por el momento nos preocupare mos de la definición de variable estructura.
Definición de variables de estructura La palabra “estructura” se utiliza en dos sentidos. El primero es el senti do “patrón de estructura”, que acabamos de discutir; el patrón es un plano sin edificio; simplemente indica al compilador cómo hacer una cosa determi nada, pero no le da los materiales para que el ordenador puede hacerla. La siguiente etapa es la creación de una “variable de estructura”; es también el segundo sentido de la palabra. La línea de nuestro programa que crea una variable de estructura es struct biblio libro;
struct biblio libro;
en la que se declara libro como estructura de tipo biblio. Más tarde aparece la listas de “miembros” de la estructura, encerrados entre un par de llaves; cada miembro queda descrito en su propia declaración; por ejemplo, la por ción título es un array de char con MAXTIT elementos; los miembros pue den ser de cualquiera de los tipos de datos que ya hemos mencionado, inclu
Figura 14.1 Disposición de memoria para una estructura 395
Una vez recibida esta instrucción, el ordenador crea la variable libro. Siguiendo los planos indicados en biblio, la máquina prepara espacio de almacenamiento para un array de char con MAXTIT elementos, un array de char de MAXAUT elementos y una variable float. Todo este espacio se coloca en el mis mo “cesto” bajo el nombre libro. (En la siguiente sección indicaremos cómo “sacar del cesto” las variables por separado.) En nuestra declaración, struct biblio juega el mismo papel que int o float hacen en una declaración normal. Por ejemplo, podemos declarar dos varia bles de tipo struct biblio, o incluso un puntero a esta clase de estructura: struct biblio unamuno, baroja, *ptlibro;
Las variables unamuno y baroja tendrán cada una de ellas su parte de título, autor y precio. Por su parte, el puntero ptlibro puede apuntar a una muno, baroja o cualquier otra estructura biblio. Por lo que respecta al ordenador, la sentencia struct biblio libro;
¿Se puede hacer lo mismo con una variable estructura? Sí, siempre que esta variable estructura sea externa o estática. Lo que se debe tener en cuenta aquí es que, dependiendo de donde se defina la variable, no de donde esté definido el patrón, una variable estructura es o no externa. En nuestro ejem plo, el patrón biblio es externo, pero la variable libro no lo es, ya que se ha definido dentro de la función, y, por defecto, se le asigna una clase de alma cenamiento automática. Supongamos, no obstante, que hemos hecho la si guiente declaración: static struct biblio libro;
Con una clase de almacenamiento estática, podemos inicializar la estruc tura de la siguiente forma: static struct biblio libro = { "El Pirata y el Vaquero", "Rimsky Korsakoff", 125. 5 };
es una forma abreviada de escribir struct biblio { char titulo[MAXTIT]; char autor[MAXAUT]; float precio; }
libro; /* une el nombre de la variable al patron */
Dicho de otra forma, el proceso de definición del patrón de estructura y de definición de la variable de estructura pueden combinarse en una sola etapa. La combinación de definiciones de patrón y variables permite evitar, si se desea, el uso del “rótulo” (tag): struct { /* sin etiqueta */ char titulo [MAXTIT]; char autor[MAXAUT]; float precio; } libro;
Por su parte, la forma con rótulo es mucho más manejable si se va utili zar el mismo patrón de estructura más de una vez. Existe un aspecto en la definición de una variable estructura que no apa rece en nuestro ejemplo: la inicialización. Daremos un breve repaso a este punto.
Con el fin de hacer más obvia las asociaciones, hemos colocado cada miem bro en su propia línea de inicialización; téngase en cuenta, sin embargo, que lo único que necesita el compilador son comas que separen la inicialización de un miembro del siguiente. Una vez aclarado este punto, continuaremos nuestro recorrido por las pro piedades de las estructuras.
Cómo acceder a miembros de la estructura Una estructura es una especie de superarray, en la cual un elemento pue de ser de tipo char; el siguiente, float, y el siguiente, int. Hasta ahora hemos accedido a los elementos individuales de un array utilizando un subíndice. ¿Cómo podemos hacer lo mismo con los miembros de una estructura? Em pleando un el operador de miembro de estructura. Por ejemplo, libro.precio es la porción precio de libro. Se puede utilizar libro.precio exactamente igual que cualquier otra variable de tipo float; de igual forma se puede usar libro.título de manera idéntica a un array de char normal. Así, podemos em plear expresiones como
Inicialización de una estructura
gets(libro.titulo);
Ya vimos con anterioridad cómo inicializar variables y arrays int cont = 0; static int piso[] = {0, 1, 1, 2, 3, 5, 8};
y scanf("%f", &libro.precio); 397
{
printf("Introduzca ahora el autor.\n"); gets(libro[cont].autor); printf("Ahora ponga el precio. \n"); scanf("%f", &libro[cont++].valor) ; while (getchar() != '\n'); /* limpia linea entrada */ if (cont < MAXLIB) printf("Introduzca el siguiente titulo.\n");
En esencia, .título, .autor y .precio juegan el papel de subíndices en la estructura biblio. Si tenemos una segunda variable de estructura del mismo tipo, se puede emplear el mismo sistema:
}
struct libro historia, geografia; gets(historia.titulo); gets(geografía.titulo);
} El .título se refiere siempre al miembro de la estructura biblio. Observe que en nuestro programa inicial hemos impreso el contenido de la estructura libro en dos formatos diferentes; queríamos ilustrar con ello la libertad de que se dispone en la utilización de miembros de la estructura. Con esto terminamos la parte básica. Ahora ampliaremos nuestro terri torio de caza estudiando algunas ramificaciones del problema central de las estructuras, incluyendo array de estructuras, estructuras de estructuras, pun teros y estructuras, y funciones y estructuras.
printf("Ahi va su lista de libros:\n"); for (indice = 0; indice < cont; indice++) printf("%s por %s: %. 2f pts. \n", libro[indice].titulo, libro[indice].autor, libro[indice].valor);
Figura 14.2
Programa de inventario de libros
Un ejemplo de salida sería Introduzca el titulo del libro. Pulse [enter] a comienzo de linea para parar. Mi Vida en la Antartida
Arrays de estructuras Por de pronto, arreglaremos nuestro programa de libros para que se ajusten a las necesidades de los que poseen dos o tres libros (¡los hay que incluso tienen más!). Evidentemente, cada libro puede describirse por medio de una variable de estructura del tipo biblio; si queremos describir dos biblios necesi taremos utilizar dos variables de este tipo, etc.; si queremos trabajar con va rios o muchos libros necesitaremos, sin duda, un array de estructuras como la original, que es lo que hemos creado en el programa presentado en la figu ra 14.2. /* inventario de varios libros */ #include #define MAXTIT 40 #define MAXAUT 40 #define MAXLIB 100 /* numero maximo de libros */ #define STOP "" /* tira nula, finaliza entrada */ struct biblio { /* prepara patron estructura */ char titulo[MAXTIT]; char autor[MAXAUT]; float valor;
};
main()
{ struct biblio libro[MAXLIB] ; /* array de estructuras biblio */ int cont = 0; int indice; printf("Introduzca titulo del libro.\n"); printf("Pulse [enter] a comienzo de linea para parar.\n") ; while (strcmp(gets(libro[cont].titulo),STOP) != 0 && cont < MAXLIB)
Introduzca ahora el autor. Ramon Caluroso
Ahora ponga el precio 333.0
Introduzca el siguiente titulo. . . . mas entradas . . .
Ahi va su lista de libros: Mi Vida en la Antartida por Ramon Caluroso: 333.00 pts. Razon y Sinrazon por Aristoteles Wodow: 1125.00 pts. Los Animales Racionales por Elena Feminiskaya: 380.50 pts. Aerobic a todas horas por Juan Deltoides: 1500.50 pts. Sistema Operativo UNIX por Waite, Martin y Prata: 2300.00 pts. Como hacer Copias Piratas por Mr. Xerox: 500.00 pts. Sensualidad y Filosofia por Nadia Limonskowska: 1000.25 pts. El Destino va en Bikini por Anselmo Chapuzon: 895.00 pts. La Historia de Tirania por Waldo Astoriampfz: 9995.00 pts. Domine su Reloj Digital por Yaguchi Kamamoto: 1350.00 pts.
Los dos puntos más importantes a observar en un array de estructuras son: cómo han de declararse y cómo se puede acceder a los miembros indivi duales; una vez explicados estos detalles, volveremos a considerar el progra ma para fijarnos en algunos de sus aspectos más interesantes. Declaración de un array de estructuras
El proceso de declaración de un array de estructura es completamente aná logo al de cualquier otro tipo de array: struct biblio libro[MAXLIB]; 399
Y ya que estamos con esto, intente imaginar cuál es el valor de Con ello se declara libro como un array de MAXLIB elementos. Cada ele mento del array es una estructura del tipo biblio; por tanto, libro[0] es una estructura del tipo biblio, libro [1] una segunda estructura, etc. La figura 14.3 puede ayudarle a visualizar la idea. El nombre libro per se no es un nombre de estructura: es el nombre del array que alberga las estructuras.
libro[2].titulo[4]
sería el quinto elemento del título (es decir, el indicado por título[4]) del libro descrito en la tercera estructura (por aquello de ser libro[2]). En nuestro ejemplo es concretamente el carácter A. Creemos que con esto queda claro que los subíndices escritos a la derecha del operador se aplican a los miembros individuales, en tanto que los subíndices a la izquierda del operador se apli can al array de estructura. Volvamos ahora con el programa. Detalles del programa
La alteración principal que hemos introducido con respecto al primer pro grama es la instalación de un bucle para leer libros sucesivamente. Comenza mos el bucle con esta condición while: while (strcmp
F igu ra 14.3
Un array de estructuras
La expresión gets(libro[count].título) lee una tira de entrada como título del libro. La función strcmp( ) compara esta tira de caracteres con STOP, que es simplemente una tira vacía, "", como ya habíamos utilizado antes. Si el usuario pulsa [enter] al comienzo de una línea, se transmite la tira vacía y finaliza el bucle. Disponemos también de un control para mantener el nú mero de libros leídos por debajo del límite de tamaño del array. A continuación aparece una sentencia extraña:
Identificación de los miembros en un array de estructuras
Para identificar los miembros de un array de estructuras se aplica la mis ma regla que empleamos para estructuras individuales: se escribe el nombre de la estructura seguido del operador miembro y el nombre del miembro: Libro[0]].precio es el precio asociado con el primer elemento del array Libro[4].título es el título asociado con el quinto elemento del array
Observe que el subíndice del array está unido a libro, no al final del nom bre:
while (getchar() ¡= '\n');
/* limpia linea entrada */
La razón de esta sentencia obedece a una peculiaridad de scanf( ). La fun ción scanf( ) ignora espacios y caracteres nueva línea; así, cuando se respon de a la pregunta del precio del libro, si se teclea algo como 12.50[enter]
Se transmitirá a la secuencia de caracteres 12.50\n
libro.precio[2] libro[2].precio
/* INCORRECTO */ /* CORRECTO */
Utilizamos libro[2].precio porque libro[2] es precisamente el nombre de la variante de estructura, al igual que libro[l] es otro nombre de variable de estructura, como antes lo fue Cervantes.
La función scanf( ) recoge el 1, el 2, el ., el 5 y el 0 pero deja allí el \ n esperando a la próxima sentencia de lectura. Si no hubiésemos incluido nues tra línea extraña, la próxima sentencia de lectura sería gets(libro[cont.].títu lo) en la sentencia de control del bucle; por consiguiente, leeríamos el carác ter nueva línea abandonado como primer carácter, y el programa pensaría que hemos enviado una señal de stop. Esta es la razón de nuestra extraña 401
La salida sería: sentencia. Si la observa con cuidado, comprenderá que devora caracteres hasta que encuentra y elimina el carácter nueva línea; por lo demás, no realiza nin guna función con la excepción de eliminar dicho carácter de la cola de entra da. Con ello se permite que gets( ) disponga de un comienzo “fresco”. Para cerrar el círculo, nos dedicaremos ahora de nuevo a explorar las po sibilidades de las estructuras.
Querido Pepe, Gracias por esa tarde maravillosa, Pepe. Me has demostrado que realmente un sexador de pollos no es una persona corriente, A ver si quedamos frente a un delicioso plato de alcachofas y pasamos otra buena velada. Hasta pronto, Juanita
Estructuras anidadas A veces es conveniente disponer de estructuras contenidas o “anidadas” en otras. Por ejemplo, Juanita Muchamarcha está preparando una estructu ra que contiene información sobre sus amigos. Uno de los miembros de la estructura, naturalmente, es el nombre de su amigo; sin embargo, el nombre puede quedar representado por sí mismo en una estructura en la que se dis ponga de entradas separadas para el nombre y el apellido. La figura 14.4 es un ejemplo resumido del trabajo de Juanita. /* ejemplo de estructura anidada */ #define LEN 20 #define M1 " Gracias por esa tarde maravillosa, " #define M2 "Me has demostrado que realmente un " #define M3 "no es una persona corriente, A ver si quedamos" #define M4 "frente a un delicioso plato de " #define M5 " y pasamos otra buena velada.” struct nombres { / * primer patron de estructura */ char nom[LEN]; char apell[LEN];
La primera observación a realizar es la forma en que la estructura anida da se coloca en el patrón. Simplemente se declara, al igual que se haría con una variable int: struct nombres maneja;
Con ello se indica que maneja es una variable del tipo struct nombres. Por supuesto, el fichero debe incluir también el patrón correspondiente de la estructura nombres. El segundo punto a anotar es cómo conseguir acceder a los miembros de una estructura anidada. Sencillamente se utiliza el operador dos veces: feten.maneja.nom == "Pepe"
La construcción se interpreta de la siguiente forma, yendo de izquierda a derecha:
}; struct tio
main()
{ /* segundo patron */ struct nombres maneja; /* estructura anidada */ char comifavo[LEN] ; char trabajo[LEN]; float gana;
};
{
}
static struct tio feten = { /* inicializa variable */ {"Pepe", "Gafe"}, "alcachofas", "sexador de pollos", 3535000.00 }; printf("Querido %s, \n\n”, feten. maneja.nom) ; printf( "%s %s. \n", M1, feten. maneja.nom) ; printf( ”%s %s\n", M2, feten.trabajo) ; printf("%s\n", M3) ; printf( "%s %s\n%s\n\n'’, M4, feten.comifavo, M5) ; printf("%40s%s\n", " ", "Hasta pronto,"); printf("%40s%s\n", " ", "Juanita"); Figura 14.4
Programa con estructuras anidadas
(feten.maneja).nom
es decir, primero se busca fetén; a continuación, el miembro maneja de fe tén, y después, el miembro nom de este último. Para el próximo acto presentaremos la actuación de nuestros famosos ar tistas los punteros.
Punteros a estructuras Los forofos de los punteros se alegrarán de saber que se pueden utilizar punteros a estructuras. La noticia es buena, al menos por tres razones: pri mera, al igual que sucedía con los punteros a arrays, que resultaban más fá ciles de manipular (en un problema de ordenación, por ejemplo) que los pro pios arrays, los punteros a estructuras son más sencillos de manejar que las estructuras en sí mismas; segunda, una estructura no puede pasarse como ar gumento a una función, pero el puntero a estructura sí puede; tercera, exis ten muchas representaciones de datos realmente elaboradas que son estruc turas que contienen punteros a otras estructuras. 403
Declaración e inicialización de un puntero a estructura
En el siguiente ejemplo (figura 14.5) se muestra cómo definir un puntero a una estructura y cómo utilizarlo para acceder a los miembros de la estruc tura. /* puntero a estructura */ #define LEN 20 struct nombres { char nom[LEN]; char apelI[LEN];
};
struct tio
main()
{ struct nombres maneja; /* estructura anidada */ char comifavo[LEN]; char trabajo[LEN]; float gana;
};
{
static struct tio feten[2] = { { {"Pepe", "Gafe"}, "alcachofas", "sexador de pollos", 3535000.00 }, { {"Santi", "Fever"}, "salmon ahumado", "programador", 9999995. 00
}
};
struct tio *este; /* AQUI ESTA: puntero a estructura */
}
printf("direccion 1: %u; 2 : %u\n", &feten[1], &feten[2] ); este = &feten[0] ; /* indica al puntero donde apuntar */ printf("puntero i: %u; 2: %u\n", este, este + 1); printf ("este->gana vale %.2f: (*este).gana vale %.2f\n", este->gana, (*este).gana ); este++ / * apunta a la siguiente estructura * / printf("este->comifavo es %s: este->nombres.apell es %s\n", este->comifavo, este->maneja.apell); Figura 14.5
Programa con un puntero a una estructura
Ya que insisten, les mostraremos la salida: direccion 1:12; 2 : 96 puntero 1: 12; 2 : 96 >gana vale 3535000.00: (*este).gana vale 3535000.00 este —> comifavo es salmon ahumado: este-> nombres.apell es Fever
Observemos primero cómo se crea un puntero a la estructura tío; después nos dedicaremos a estudiar cómo se especifican los miembros individuales de una estructura por medio del puntero.
La declaración no puede ser más fácil: struct tio *este;
En primer lugar se coloca la palabra clave struct; después, la etiqueta del patrón tío y un * seguido del nombre del puntero. La sintaxis es la misma que la utilizada en otras declaraciones de punteros ya vistas. El puntero este se puede preparar para que apunte a cualquier estructura del tipo tío. Inicializamos este haciéndolo apuntar a fetén[0]; obsérvese que hemos empleado el operador dirección: este = &feten[0];
Las dos primeras líneas de salida muestran que esta asignación ha sido correcta. Comparando las dos líneas, observamos que este apunta a fetén[0] y este + 1 apunta a fetén[1]. Observe que sumando 1 a este se suman 84 a la dirección. Se debe a que cada estructura tío ocupa 84 bytes de memoria: 20 para el nombre, 20 para el apellido, 20 para comifavo, 20 para trabajo y 4 para gana, que es el tamaño de un float en nuestro sistema. Acceso a miembros por puntero
Tenemos a este apuntando a la estructura fetén[0]. ¿Cómo podemos ha cer que este consiga extraer un valor de un miembro de fetén[0]? La tercera línea de salida muestra dos métodos para hacerlo. El primer método, el más común, utiliza un nuevo operador, — > . Este operador se forma tecleando un guión (-) seguido del símbolo “mayor que” ( > ) . El ejemplo siguiente le ayudará a aclarar ideas: este-> gana es feten[0].gana
cuando este
=
&feten[0]
Dicho de otra manera, un puntero a estructura seguido del operador -> funciona exactamente igual que un nombre de estructura seguido del opera dor (No podemos referirnos con propiedad a este.gana porque este no es un nombre de estructura.) Es importante señalar que este es un puntero, pero este-> gana es un miembro de la estructura apuntada. En este caso, por tanto, este-> gana es simplemente una variable del tipo float. El segundo método para especificar el valor de un miembro de una es tructura obedece a la siguiente secuencia: si este == &fetén[0], se cumplirá que *este = = feten[0]. Se debe a que & y * son dos operadores recíprocos. Por tanto, feten[0].gana == (*este).gana
por sustitución. Es necesario el uso de paréntesis porque el operador tie ne mayor preferencia que el operador *. 405
En resumen, si definimos este como puntero a la estructura fetén[0], se cumple la siguiente equivalencia feten[0].gana == (*este).gana == este->gana
Nos dedicaremos a continuación al problema de la interacción entre es tructuras y funciones. RESUMEN: OPERADORES
DE ESTRUCTURAS Y UNIONES
I. El operador de pertenencia: Este operador se emplea, junto con un nombre de unión o estructura, para especificar un miembro de las mismas. Si nombre es el nombre de una estruc tura, y miembro, un miembro especificado por el patrón de estructura, nombre.miembro
identifica a dicho miembro de la estructura. Este operador de pertenencia pue de usarse de igual manera con uniones. Ejemplo:
struct { int codigo; float precio; } articulo; articulo.codigo = 1265;
Cómo enseñar estructuras a las funciones Recordemos que los argumentos de una función pasan valores a dicha función. Cada valor es un número, sea de tipo int, de tipo float, o quizá un código ASCII o una dirección. Las estructuras son algo más complicadas que un valor sencillo, por lo que no resulta sorprendente que una estructura como tal se pueda enviar como argumento a una función. (Por cierto, esta limitación está siendo eliminada en las últimas implementaciones.) Sin em bargo, existen formas de enviar información acerca de la estructura a una función. Estudiaremos tres métodos (en realidad, dos con variaciones) en es te párrafo.
Utilización de los miembros de la estructura Al ser los miembros de la estructura variables simples (por ejemplo, un int o alguno de sus parientes, un char, un float, un double o un puntero), se pueden utilizar como argumentos de funciones. El listado de la figura 14.6 es un programa de análisis financiero bastante primitivo, el cual suma las can tidades depositadas por un cliente en su cuenta corriente y libreta de ahorro. De paso, observe que hemos combinado la definición del patrón, la declara ción de variables y la inicialización en una sola sentencia. /* paso de miembros de estructura a una funcion */ struct fondos { char *banco; float ccorri; char *ahorro; float cahorro; } garcia = { "Banco Pacifico", 102343.25, "Banco de Poniente", 423987.21 };
Esta operación asignaría un valor al miembro código de la estructura artículo. II. El operador de pertenencia indirecto, — > : Este operador se usa con un puntero a estructura, o a unión, para iden tificar un miembro de la misma. Supongamos que ptrstr es un puntero a estructura, y que miembro es un miembro especificado de aquélla por el patrón correspondiente. En tal caso ptrstr->miembro
identifica a dicho miembro de la estructura apuntada. Este operador de pertenencia indirecta se puede emplear de la misma forma con uniones. Ejemplo: struct { int codigo; float precio; } articulo, *ptrst;
printf("Garcia tiene un total de %2f pts. \n", suma(garcia.ccorri, garcia.cahorro) );
} /* funcion que suma dos numeros float */ float suma(x,y) float x,y;
ptrst = &articulo; ptrst->codigo = 3451;
Con ello se asignaría un valor al miembro código de artículo. Las tres expresiones siguientes son equivalentes ptrst->codigo
articulo.codigo
main() { float total, suma() ; extern struct fondos garcia; /* declaracion opcional */
return(x + y);
}
(*ptrst).codigo F igu ra 14.6
Programa que pasa miembros de estructura como argumento de función 407
El resultado de la ejecución de este programa es Garcia tiene un total de 526330.46 pts.
¡Funciona! Observe que la función sum( ) no sabe ni se preocupa si los argumentos enviados son miembros de una estructura o no; simplemente re quiere que sean del tipo float. Por supuesto, si deseamos que un programa modifique el valor de un miem bro del programa de llamada, podemos enviar la dirección de dicho miem bro: modifica(&garcia.cahorro);
podría muy bien ser una función que modificase el estado de la cuenta del señor García. El siguiente modus operandi en interacciones entre funciones y estructu ras implica hacer saber a la función que está tratando con una estructura. Utilización de la dirección de la estructura
Vamos a resolver el mismo problema que antes, sólo que esta vez utiliza remos la dirección de la estructura como argumento. Este sistema es correc to, ya que la dirección es un simple número; sin embargo, la función tendrá que trabajar con la estructura fondos; por tanto, deberá hacer uso del pa trón fondos también. En la figura 14.7 se presenta el programa.
También aquí se produce una salida idéntica Garcia tiene un total de 526330.46 pts.
paso de la direccion de la estructura a una funcion */ struct fondos { char *banco; float ccorri; char *ahorro; float cahorro; /*
} garcia = {
"Banco Pacifico", 102343.25, "Banco de Poniente", 423987.21,
};
main() { float total, suma(); printf( "Garcia tiene un total de %.2f pts. \n”, suma(&garcia));
La función suma( ) tiene un puntero (dinero) a una estructura fondos. Al pasar la dirección &garcía a la función se consigue que el puntero dinero apunte a la estructura garcía. Podemos entonces utilizar el operador -> para acceder a los valores garcía.cahorro y garcía.ccorri. Esta función podría también acceder al propio nombre del banco, aun que no lo utiliza. Observe que debemos emplear el operador & para indicar la dirección de la estructura. A diferencia del nombre del array, el nombre de la estructura en solitario no es un sinónimo de su dirección. El siguiente método se aplica a un array de estructuras, y es una variación del que acabamos de exponer.
} Utilización de un array float suma(dinero) struct fondos *dinero;
{
}
return(dinero->ccorri + dinero->cahorro); Figura 14.7
Programa que utiliza la dirección de la estructura en una función
Supongamos que tenemos un array de estructuras. El nombre de un array es un sinónimo de su dirección; por tanto, puede pasarse a una función. La función necesitará, de nuevo, poder acceder al patrón de estructura. Para demostrar el funcionamiento de este sistema (figura 14.8), expandiremos nues tro programa a dos personas, por lo que, en total, tendremos un array de dos estructuras fondos. 409
/* paso de un array de estructuras a una funcion */ struct fondos { char *banco; float ccorri; char *ahorro; float cahorro; } garcias[2 = {
{
"Banco Pacifico", 102343.25, "Banco de Poniente". 423987.21 },
{ "Banca La Honradez", 97656.50, "Banco Cantonal", 176013.04 }
}; main() { float total, suma(); printf("Los garcia tienen un total de .2f pts.\n", suma(garcias) );
} float suma(dinero) struct fondos *dinero;
{
float total; int i ; for (i=0, total = 0; i < 2; i++, dinero++) total += dinero->ccorri + dinero-> cahorro; return(total);
Figura 14.8
Programa que envía un array de estructuras a una función
ción, el bucle for incrementa el puntero dinero en 1; ahora apunta a la si guiente estructura, garcias[1], posibilitando que se sumen el resto de cantida des al total. En este ejemplo hay dos detalles a destacar: 1. Se puede usar el nombre del array para pasar a la función un puntero apuntando a la primera estructura del propio array. 2. A continuación podemos emplear un puntero aritmético para mover el puntero anterior a las demás estructuras del array. Observe que la llamada a función suma(&garciast[0]);
habría tenido el mismo efecto que si hubiésemos empleado el nombre del array, ya que ambos se refieren a la misma dirección. La utiliza ción del nombre del array es simplemente un método indirecto de pa sar la dirección de la estructura.
Y después de las estructuras, ¿qué? No queremos extendernos más en la explicación de estructuras; sin em bargo, debemos mencionar uno de los usos más importantes de las mismas: la creación de nuevos formatos de datos. Los usuarios de ordenadores han creado formatos de datos que, para ciertos problemas, resultan mucho más eficientes que los arrays y estructuras simples que hemos presentado aquí. Estos formatos tienen nombres tales como colas, árboles binarios, pilas, ta blas y gráficos. Muchos de ellos se construyen a partir de estructuras “enca denadas”. Como caso típico, cada estructura contiene uno o dos datos y, además, uno o un par de punteros que apuntan a otras estructuras del mismo tipo. Los punteros sirven para encadenar una estructura a la siguiente y, al
La salida: Los Garcia tienen un total de 800000.00 pts.
(¡Nos ha cuadrado la suma! ¡Y encima es un número redondo! Cualquie ra pensaría que nos hemos inventado los números.) El nombre del array garcias es un puntero al array. En concreto, apunta al primer elemento del array, que es la estructura garcias[0]. Así, el puntero dinero está definido inicialmente como dine ro = & garcia s[0];
El empleo del operador — > nos permite sumar las dos cantidades del primer García. Hasta aquí es muy parecido al ejemplo anterior. A continua
Figura 14.9
Una estructura en árbol binario 411
tiempo, para fabricar un camino que permita un rastreo por la estructura glo bal. Por ejemplo, la figura 14.9 muestra una estructura en árbol binario, en la que cada estructura individual (o “nodo”) se conecta con dos situadas por debajo de la misma. Pero ¿es que este engendro ramificado es más eficiente que un array? Bien, consideremos el caso de un árbol con 10 niveles de nodos. Si lo observa con atención, encontrará que existen 1023 nodos, en los cuales se pueden almace nar, por ejemplo, 1023 palabras. Si las palabras se organizan siguiendo una pauta coherente, se puede comenzar en el nivel superior y encontrar cualquier palabra en 9 movimientos como máximo, según se va bajando de un nivel al siguiente. Si tuviese las palabras colocadas en un array, podría, en caso de desgracia extrema, tener que rastrear los 1023 elementos antes de tropezar con la palabra deseada. Si está interesado en estructuras de datos avanzadas, le aconsejamos que consulte un libro de Ciencias del Cómputo. Las estructuras del C le permiti rán reproducir los formatos que allí encuentre. Aquí ponemos punto final a las estructuras. Seguidamente, daremos un somero repaso a otros dos modos de tratamientos de datos en C: las uniones y typedef.
Un vistazo rápido a las uniones Una unión es un instrumento que permite almacenar tipos de datos dife rentes en el mismo espacio de memoria. Una utilidad típica de tal sistema sería la creación de una tabla que pudiese guardar una mezcla de tipos en un orden arbitrario, que en principio no va a ser regular ni conocido. La unión permite la creación de un array de unidades del mismo tamaño, en las que cada una de ellas contiene datos de un tipo distinto. Las uniones se preparan de una forma muy semejante a las estructuras. Existe el patrón de la unión y las variables de la misma. Se definen de una sola vez, o bien utilizando una etiqueta de unión. El siguiente es un ejemplo de patrón con etiqueta: union agarra { int numero; double grande; char letra;
};
La definición
La primera declaración crea una variable única llamada ficha. El compi lador prepara espacio suficiente para almacenar la mayor de las posibilida des descritas. En nuestro ejemplo, la mayor posibilidad entre las especifica das es double, que necesita 64 bits, u 8 bytes, en nuestro sistema. El array guarda tendrá 10 elementos, cada uno de 8 bytes. Las uniones se emplean de la siguiente manera: ficha.numero = 23; ficha.grande = 2.0; ficha.letra = 'h';
se guarda 23 en ficha usando 2 bytes */ /* borra 23; guarda 2.0 usando 8 bytes */ /* borra 2.0; guarda h usando 1 byte */ /*
Para especificar el tipo de datos que se está utilizando, se usa el operador de pertenencia. Sólo se guarda un valor en cada momento; no se pueden al macenar un char y un int a la vez, incluso cuando, como en este caso, hay espacio suficiente para ello. Queda bajo su responsabilidad llevar la cuenta del tipo de datos que se está usando en la unión; la secuencia siguiente muestra lo que no se debe hacer: ficha.letra = 'A'; flnum = 3.02 * ficha.grande;
/*
ERROR ERROR ERROR */
Este fragmento de programa sería erróneo, porque se ha almacenado un tipo char, mientras que en la siguiente línea se presupone que el contenido de ficha es del tipo double. Se puede emplear el operador - > con uniones de igual manera a como se hacía con las estructuras: pu = & ficha;
x = pu->numero;
/* equivale a x = ficha.numero */
Estudiemos ahora otro aspecto de la organización avanzada de datos en C.
Otro vistazo a typedef La palabra typedef permite crear un tipo con un nombre arbitrario otor gado por el usuario. Se parece bastante a #define en este aspecto; sin embar go, tiene tres diferencias: 1. Al contrario que #define, typedef está limitado a otorgar nombres sim
bólicos a tipos de datos únicamente. 2. La función typedef se ejecuta por compilador, no por preprocesador. 3. Dentro de sus límites, typedef es más flexible que #define.
de variables de una unión de tipo agarra sería:
union agarra ficha; /* variable union de tipo agarra */ union agarra guarda[10]; /* array de 10 variables union */ union agarra *coloca /* puntero a una variable agarra */
Veamos cómo funciona. Supongamos que desamos usar la palabra real en lugar de float. Para ello se define real como si fuese una variable float, y se antecede la definición con la palabra clave typedef: typedef float real; 413
A partir de este momento, se puede usar real para definir variables: real x, y[25], *pr;
El alcance de esta definición depende de la localización de la sentencia typedef. Si la definición se realiza dentro de una función, el alcance queda
confinado a la misma. Si la definición es externa a la función, el alcance es global. A menudo, se emplean letras mayúsculas para estas definiciones, con el fin de recordar al usuario que el nombre del tipo es en realidad una abrevia tura simbólica: typedef float REAL;
Este último ejemplo podría haberse realizado de igual forma con un #define. Sin embargo, el que presentamos a continuación no puede hacerse
así:
hace que FRPTC sea un tipo en el que una función devuelve un puntero a un array de char de cinco elementos. (Vea en el cuadro hasta dónde puede llegar nuestra caja de sorpresas de declaraciones.) Un tercer motivo para el uso de typedef es hacer los programas más trans portables. Por ejemplo, supongamos que su programa necesita usar núme ros de 16 bits. En algunos sistemas esto significaría un tipo short; en otros puede ser tipo int. Si se usara simplemente short o int en las declaraciones, habría que alterar todas ellas para cambiarse de un sistema a otro; en su lu gar, haga lo siguiente: en un fichero #include introduzca la siguiente defini ción typedef short DOSBYTES;
Ahora podrá usar DOSBYTES en el programa que define las variables short de 16 bits. Cuando cambie el programa al otro sistema, en el que se necesita tipo int en su lugar, simplemente cambie la definición del fichero #include: typedef int DOSBYTES;
typedef char *STRING;
Sin la palabra typedef, STRING quedaría identificado como un puntero a char. Cuando se incluye la palabra, se hace que STRING sea un identificador de punteros a char. Por tanto,
Este es un ejemplo más de las características del C como lenguaje trans portable. Cuando se use typedef, hay que tener presente que no se están creando tipos nuevos, sino simplemente utilizando etiquetas cómodas.
STRING nombre, signo;
DECLARACIONES CURIOSAS
significa char *nombre, *signo;
También se puede usar typedef con estructuras; por ejemplo: typedef struct COMPLEX { float real; float imag;
El C permite la creación de formas de datos muy elaboradas. Estamos tratando simplemente las formas más sencillas, pero creemos nuestro deber informarle del cúmulo de posibilidades que se presentan. Cuando se hace una declaración, el nombre (o “identificador”) que usamos se puede modi ficar añadiéndole un modificador:
};
permite usar el tipo COMPLEX para representar números complejos. Una de las razones que aconseja el empleo de typedef es que aquí se pue den crear nombres convenientes y reconocibles para los tipos que se utilizan más a menudo. Por ejemplo, muchos usuarios (quizá expertos en otros len guajes) prefieren usar STRING o su equivalente, tal como hicimos anterior mente. Además, los nombres typedef se usan frecuentemente para describir ti pos complicados; por ejemplo, la declaración typedef char *FRPTC() [5];
Modificador
Significado
* () []
indica un puntero indica una función indica un array
En C se puede emplear más de un modificador al mismo tiempo, lo que permite crear una gran variedad de tipos: int int int int int
tabla[8][8]; **ptr; *rico[10]; (*pico)[10]; *oof[3][4];
/* un array be arrays de int */ /* un puntero a un puntero a int*/ /* un array be 10 punteros a int*/ /* un puntero a array de 10 int*/ /* un array de 3 punteros a array de 4 int */ int (*uuf)[3][4]; /* un puntero a array de 3*4 int */ 415
La clave para desentrañar estas declaraciones es averiguar el orden en que se aplican los modificadores. Para ello se siguen tres reglas. 1. La prioridad de un modificador es tanto mayor cuanto más próximo esté el identificador. 2. Los modificadores [ ] y ( ) tienen mayor prioridad que *. 3. Se pueden usar paréntesis para agrupar parte de la expresión otorgán dole la máxima prioridad. Apliquemos estas reglas al ejemplo
Cómo colocar un puntero a una estructura: struct coche *ptcoche; Cómo acceder a un miembro utilizando un puntero: ptcoche—>kph Cómo enviar un miembro a una función: eval(ferrari.kph) Cómo hacer que una función conozca la existencia de una estructura: veloc(&ferrari)
Cómo construir una estructura anidada Cómo acceder a un miembro anidado de la estructura: ferrari.tasas.municip Cómo construir y usar arrays de estructuras: struct coche gm[5] Cómo preparar una unión: igual que una estructura Cómo usar typedef: typedef struct coche BOLIDO;
int *oof[3][4] ;
El * y el [3] son adyacentes a oof, y tienen mayor prioridad que [4] (regla 1). El [3] tiene mayor prioridad que el * (regla 2). Por consiguiente, oof es un array de tres elementos (primer modificador) de punteros (segundo modificador) a un array de cuatro elementos (tercer modificador) de tipo int (el tipo declarado). En el caso int (*uuf) [3][4] ;
los paréntesis hacen que el modificador * tenga mayor prioridad, por lo que uuf es un puntero, tal como se indicó en la declaración correspondiente. Estas reglas permiten también los siguientes tipos:
Cuestiones y respuestas Cuestiones
1. ¿Qué errores contiene este patrón? structure { char cuteria; int num[20]; char *todo }
2. Esta porción de programa, ¿qué imprimiría? struct casa {
char *fump(); /* funcion que devuelve un puntero a char */ char (*frump) (); / * puntero a una funcion que devuelve un tipo char */ char *flump()[3] ; /* función que devuelve un puntero a array de 3 elementos ce tipo char */ char *flimp[3]() ; /* array de 3 punteros a funcion que devuelve tipo char */
Si, además, metemos estructuras en la coctelera, las posibilidades de de claraciones llegan a ser realmente barrocas. Y las aplicaciones también, aun que dejaremos este asunto a los lectores avanzados.
Con las uniones, estructuras y typedef, el C suministra los útiles necesarios para un manejo de datos eficiente y transportable.
Hasta ahora hemos aprendido Qué es un patrón de estructura y cómo se define Qué es una etiqueta de estructura y cómo se usa Cómo definir una variable estructurada: struct coche ferrari Cómo acceder a un miembro de la estructura: ferrari.kph
float superf; int habitac; int plantas; char *direc;
}; main() { static struct casa chalet = { 156.0, 6, 1, "calle Barco 86"}; struct casa *signo; signo = &chalet; printf("%d %d\n", chalet.habitac, signo-> plantas) ; printf("%s \n", chalet.direc); printf("%c %c\n", signo-> direc[4] , chalet.direc[6] ) ; }
3. Diseñe un patrón de estructura que guarde el nombre de un mes, una abreviatura de tres letras para el mismo, el número de días del mes y el orden de éste. 4. Defina un array de 12 estructuras del tipo preparado en la cuestión anterior, e inicialícelo para un año no bisiesto. 5. Escriba una función que, dado el número del mes, devuelva el número total de días transcurridos en el año hasta llegar a ese mes. Para este ejercicio suponga que se han declarado externamente el patrón de estructura y el array de las cues tiones 3 y 4. 6. Dado el siguiente typedef, declare un array de 10 elementos de la estructura indi cada. A continuación, utilizando asignación individual de miembros, haga que 417
Ejercicios el tercer elemento describa una lente Remarkatar de distancia focal 500 mm. y apertura f/2.0. typedef struct { float distfoc; float apert; char *marca; } LENTE;
/* definidor de lentes */ /* distancia focal,mm*/ /* apertura */ /* marca comercial */
Respuestas
1. Vuelva a rehacer la cuestión 5, utilizando como argumento el nombre completo del mes en lugar de su número de orden. (No olvide strcmp( ).) 2. Escriba un programa que solicite del usuario un día, mes y año. El mes puede ser un número de mes, un nombre de mes o una abreviatura del mismo. El pro grama deberá devolver el número total de días transcurridos en ese año hasta la fecha indicada. 3. Revise nuestro programa de clasificación de libros de manera que imprima las descripciones de libros alfabetizados por títulos y, además, calcule la suma total de sus precios.
1. La palabra clave es struct, no structure. El patrón necesita, o bien una etiqueta antes de la llave de abrir, o un nombre de variable tras la llave de cierre. Además, debe haber un punto y coma tras *todo y otro al final del patrón. 2.
6 1
calle Barco 86 e B
El miembro chalet.direc es una tira de caracteres, y chalet.direc[4] es el quinto elemento de este array. 3. st r uct mes { char nombre[ll]; char abrev[4]; int dias; int numes;
/* o char *nombre; */ /* o char *abrev; */
};
4. struct mes meses[12] = { {"Enero", "Ene", 31, 1}, {"Febrero", "Feb", 28, 2}, ...Etcetera... { "Diciembre", "Dic”, 31, 12}
}; 5. di as( mes) i nt mes;
{
int indice, total; i f ( mes <1 | | mes > 12) return(-l); /* signo de error */ else for (indice = 0, total = 0; indice < mes; indice++) total += meses[indice].dias; return(total);
} Observe que índice es uno menos que el número de mes, ya que los arrays comienzan con el subíndice 0; por ello usamos la comparación índice < mes en lugar de índice <= mes. 6. LENTE gaf as[ 10] ; gafas[2].distfoc = 500.0; gafas[2]. apert = 2.0; gafas[2].marca = "Remarkatar" ;
419
15 La biblioteca C y el fichero de entrada/salida En este capítulo encontrará: • Cómo acceder a la biblioteca C • Acceso automático • Inclusión de ficheros • Inclusión de bibliotecas • Funciones de biblioteca que hemos utilizado • Comunicación con ficheros • ¿Qué es un fichero? • Un programa sencillo de lectura de ficheros: fopen( ), fclose( ), getc( ) y putc( ) • Apertura de un fichero: fopen( ) • Cierre de un fichero: fclose( ) • Ficheros de texto con buffer • Fichero de E/S: getc( ) y putc( ) • Un programa sencillo de reducción de ficheros • Fichero de E/S: fprintf( ), fscanf( ) y fputs( ) • Las funciones fprintf( ) y fscanf( ) • La función fgets( ) • La función fputs( ) • Acceso aleatorio: fseek( ) • Comprobación y conversión de caracteres • Conversión de tiras de caracteres: atoi( ) y atof( ) • Salida: exit( ) • Asignación de memoria: malloc( ) y calloc( ) • Otras funciones de biblioteca • Conclusión • Hasta ahora hemos aprendido • Cuestiones y respuestas • Ejercicios 421
La biblioteca C
ciones de biblioteca han de buscarse en varios sitios diferentes. Por ejemplo, getchar( ) está definido usualmente como macro en el fichero stdio.h, en tanto que strlen( ) generalmente se guarda en un fichero de biblioteca. En segundo lugar, diferentes sistemas pueden tener diferentes modos de acceso a estas funciones. A continuación presentamos tres posibilidades. Acceso automático
En muchos sistemas UNIX “grandes” simplemente se compila el progra ma, y las funciones de biblioteca más comunes son accesibles automática mente.
CONCEPTOS La biblioteca C Ficheros en C Funciones de manejo de ficheros Macros de comprobación de caracteres Funciones de asignación de memoria
Inclusión de ficheros
Si una función está definida como macro, se deberá utilizar un #include del fichero correspondiente que contenga su definición. A menudo, las fun ciones que realizan tareas semejantes se reúnen en un mismo fichero de enca bezamiento, con un nombre adecuado. Por ejemplo, muchos sistemas tienen un fichero ctype.h que contiene varias macros cuya misión es determinar la naturaleza de un carácter; mayúscula, dígito, etc. Inclusión de bibliotecas
Desde el precioso instante en que comenzamos a emplear funciones como strlen( ) estamos, en realidad, utilizando la biblioteca C. La biblioteca C contiene docenas de funciones y macros construidas para la comodidad del usuario. Las bibliotecas pueden variar de sistema a siste ma; no obstante, existe un núcleo de funciones, llamado biblioteca estándar, que está contenido en la gran mayoría de sistemas. Examinaremos en este capítulo 15 estas funciones más comunes, concentrándonos en funciones de entrada/salida y manejo de ficheros. Sin embargo, la primera parte del capítulo se dedicará a explicar cómo se utiliza la biblioteca. printf( ), getchar( ) y
Cómo acceder a la biblioteca C En realidad, el modo de acceso a la biblioteca C depende de su sistema, por lo que aconsejamos que compruebe por sí mismo si los párrafos que van a continuación pueden aplicarse a su caso concreto. En primer lugar, las fun
En un momento dado durante la compilación o carga de un programa se debe especificar una opción de biblioteca. En nuestro sistema, por ejem plo, existe un fichero, llamado Ic.lib, que contiene versiones compiladas de las funciones de biblioteca, y se debe indicar al “linker” del IBM PC que utilice dicha biblioteca. Incluso cuando el sistema compruebe automáticamente su biblioteca estándar, pueden existir otras bibliotecas de funciones usadas con menor frecuencia, las cuales habrá que solicitar explícitamente para po der usarlas como opción en tiempo de compilación. Evidentemente, no podemos detallar las características específicas de to-
Funciones de biblioteca que ya hemos utilizado Lo que viene a continuación es una simple recopilación de las funciones utilizadas hasta ahora, con el único propósito de ahorrarle a usted el trabajo de buscarlas. En primer lugar, las funciones de E/S: getchar() putchar() gets() puts() scanf() printf()
/* captura un caracter */ /* imprime un caracter */ /* captura una linea */ /* imprime una linea */ /* captura entradas con formato */ /* imprime salidas con formato */ 423
Y éstas son las funciones de manejo de tiras de caracteres: strlen() strcmp() strcpy() strcat()
/* /* /* /*
calcula longitud de una tira */ compara dos tiras */ copia una tira en otra */ combina dos tiras en una */
A esta lista añadiremos funciones para abrir y cerrar ficheros, funciones para comunicar con ficheros, para comprobar y convertir caracteres, para convertir tiras, una función de salida y funciones para asignar memoria. Como primera providencia, nos dedicaremos al problema de las comuni caciones entre un fichero y un programa.
Comunicación con ficheros Con frecuencia un programa necesita obtener información de un fichero o colocar sus resultados en otro. Un método para hacerlo es utilizar los ope radores de reenvío, < y > . Este método resulta sencillo, pero es bastante limitado. Supongamos, por ejemplo, que desea escribir un programa inte ractivo que pida títulos de libros (¿le suena?), y que se desee guardar la lista completa en un fichero. Si se usa un reenvío, como libros > listalib
de estructura de fichero. El ejemplo presentado a continuación puede ser un caso típico, tomado de la versión IBM del Lattice C: struct _iobuf {
char *_ptr int _cnt; char *_base; char _flag; char _file;
/* puntero a buffer actual */ /* contador del byte actual*/ / * direccion base de buffer E/S /* flags de control * i /* numero de fichero */
*/
#define FILE struct _iobuf /* notacion abreviada
*/
};
Una vez más, no vamos a preocuparnos de los detalles de esta definición. Los puntos a destacar son que el fichero es una estructura y que el nombre abreviado, FILE, que aparece al final corresponde al patrón del fichero. (Mu chos sistemas usan typedef para realizar la misma correspondencia.) Así pues, un programa que trabaje con ficheros utilizará el tipo estructurado FILE pa ra hacerlo. Teniendo presente lo anterior, podremos comprender mejor las operaciones de fichero.
Un programa sencillo de lectura de fichero: fopen( ), fclose( ), getc( ) y putc( )
los mensajes interactivos también se enviarán a listalib. Con ello no sólo es tamos introduciendo material no deseado en listalib, sino que, además, el usua rio no puede ver los mensajes de las cuestiones que se supone ha de responder. Por fortuna, el C ofrece métodos más potentes de comunicación con fi cheros. Uno de ellos comporta la utilización de la función fopen( ), la cual abre un fichero; el uso de funciones especiales E/S, para leer o escribir en tal fichero; por último, el empleo de la función fclose( ), para cerrar el fiche ro en cuestión. Antes de investigar estas funciones daremos un breve repaso a la naturaleza de un fichero.
El ejemplo siguiente muestra los rudimentos del manejo de ficheros. Pa ra ello hemos preparado un programa muy limitado que lee el contenido de un fichero llamado test y lo imprime en pantalla. Inmediatamente después del programa se presenta una explicación del mismo. /* nos dice que hay en el fichero "test" */ #include main() {
¿Que es un fichero? Para nosotros, un fichero es una porción de almacenamiento, general mente en disco, con un nombre. Podemos pensar, por ejemplo, que stdio.h es el nombre de un fichero que contiene una cierta cantidad de información útil. Por lo que respecta al sistema operativo, un fichero es algo ligeramente más complicado; pero es su problema, no el nuestro. Sin embargo, debemos conocer lo que representa un fichero en un programa C. En lo concerniente a las funciones de ficheros discutidas hasta ahora, el C contempla sus fiche ros como estructuras. De hecho, el fichero stdio.h contiene una definición
FILE *in; int ch;
/* declara puntero a fichero */
if ( (in = fopen("test", "r")) != NULL) /* abre fichero test para lectura, comprueba si existe */ /* el puntero FILE apunta ahora a test */
{
while ( (ch=getc(in)) != EOF) / * toma caracter de in */ putc(ch,stdout); /* lo envia a salida estandar */ fclose(in); /* cierra el fichero */
}
}
else printf("No puedo abrir el fichero \"test\".\n"); 425
Los tres detalles más importantes a explicar son el manejo de fopen( ) y de fclose( ) y la utilización de las funciones del fichero de E/S. Vayamos por orden. Apertura de un fichero: fopen( ) Fopen( ) es una función controlada por tres parámetros básicos. El pri
mero es el nombre del fichero que se va a abrir. Este nombre se indica en forma de tiras de caracteres como primer argumento de fopen( ); en nuestro caso es “test”. El segundo parámetro (que es el segundo argumento de fopen( )) descri be el uso a que se va a destinar el fichero. Existen tres usos básicos: “r”: un fichero de lectura “w”: un fichero de escritura “a”: un apéndice del fichero Algunos sistemas ofrecen posibilidades adicionales, pero nos contentare mos con éstas. Observe que los códigos utilizados son tiras de caracteres, pe ro no constantes de tipo carácter; por ello van encerradas entre comillas. La opción “r” abre un fichero ya existente. Las otras dos opciones abren un fichero existente, si lo encuentran; si no existe, crean uno con ¿se nombre. PRECAUCION: Si se usa la opción “w” en un fichero ya existente, la ver sión antigua se borrará, con el fin de que el programa comience con el fiche ro limpio. El tercer parámetro es un puntero al fichero; su valor se devuelve en la función: FILE *in; in = fopen("test", "r");
Tras la definición, in es un puntero al fichero “test”. A partir de ese mo mento, el programa se refiere al fichero por el puntero in, y no por su nom bre test. Como usted es un lector inteligente, se le habrá planteado la siguien te duda: “Si fopen( ) devuelve un puntero ‘FILE’ como argumento, ¿por qué no tenemos que declarar fopen( ) como función de tipo puntero ‘FILE’?” Buena pregunta. La respuesta es que esta declaración está hecha en stdio.h. que contiene la línea FILE * f o p e n ( );
Cierre de un fichero: fclose( )
En nuestro ejemplo se refleja cómo cerrar un fichero: fclose(in);
Simplemente utilizando la función fclose( ). Obsérvese que el argumento es in, el puntero al fichero, y no test, el nombre del fichero. En un programa más serio que el presentado habríamos comprobado si el fichero se ha cerrado normalmente. La función fclose( ) devuelve un valor 0 en caso de cierre satisfactorio, y —1 si no lo es. Ficheros de texto con buffer
Las funciones fopen( ) y fclose( ) trabajan con ficheros de texto con buffer, Con ello queremos indicar que la entrada y salida se almacenan temporal mente en un área de memoria llamada el buffer. Cuando el buffer se llena, el contenido se traspasa a un bloque de memoria, y se recomienza el proceso. Una de las tareas principales de fclose( ) es el “vaciado” del buffer, que po dría haber quedado parcialmente lleno al cerrar el fichero. Un fichero de texto es aquel en el que la información se almacena como caracteres, utilizando un código ASCII o similar. El caso opuesto es un fi chero binario, como el que se ha de emplear para almacenar un código ara lenguaje máquina. Las funciones de E/S que vamos a describir a continuación están diseña das para trabajar únicamente con ficheros de texto. Fichero E/S: getc( ) y putc( )
Las funciones getc( ) y putc( ) se comportan de forma muy semejante a getchar( ) y putchar( ). La diferencia es que a estas dos nuevas funciones se les tiene que indicar el fichero que deben utilizar. Así, nuestro viejo amigo ch = getchar() ;
Existe una importante característica de fopen( ) que ya hemos utilizado. Si fopen( ) no consigue abrir el fichero requerido, devuelve un valor “NULL” (definido como 0 en stdio.h). ¿Y por qué no se puede abrir el fichero? Por ejemplo, si se intenta leer un fichero no existente. Esta es la razón de la línea de programa if ( (in = fopen("test", "r"))
También puede suceder que el disco esté lleno, o que el nombre sea ilegal, o cualquier otra razón que impide la apertura del fichero. Es conveniente, por tanto, comprobar los fallos de apertura; este pequeño detalle puede aho rrarnos muchos problemas. El cierre del fichero es bastante más fácil.
!= NULL)
significa que capturamos un carácter de la entrada estándar, pero ch = getc( in);
indica que se ha de tomar un carácter del fichero apuntado por in. De igual forma, putc(ch, out) ; 427
{strcpy(nombre,
copia nombre del fichero en un array */ strcat(nombre, ".red"); / * une .red al nombre * / out = fopen(nombre, "w"); /* abre otro fichero como salida en escritura */ while ( (ch = getc(in)) != EOF) if (cont++ % 3 == 0) putc(ch, out); /* envia un caracter cada tres */ fclose(in); fclose(out);
quiere decir que se envía el carácter ch al fichero apuntado por out, un pun tero de tipo FILE. La lista de argumentos de putc( ) comprende el propio carácter y a conti nuación el puntero al fichero. En nuestro ejemplo, hemos utilizado putc(ch, stdout);
argv[1]);
/*
}
donde stdout es un puntero a la salida estándar; por consiguiente, esta sen tencia es equivalente a
else printf("No puedo abrir el fichero \"%s\". \n", argv[1] ; }
} putchar(ch);
Figura 15.1
De hecho, putchar( ) está definido con un #define en stdio.h como putc(ch,stdout). Este temible fichero tiene también #define de stdout y stdin
Programa de reducción de ficheros
como punteros a la salida y entrada estándar del sistema. ¿Sencillo, verdad? Bien, añadiremos ahora otro par de pases mágicos.
Programa sencillo de reducción de ficheros En el ejemplo anterior, el nombre del fichero que se debía abrir estaba escrito dentro del programa. En realidad, esta restricción no existe: utilizan do argumentos en línea de ejecución, podemos indicar a nuestro programa el nombre del fichero que deseamos leer. En nuestro siguiente ejemplo (figu ra 15.1) procedemos de esta manera. El programa resume el contenido del fichero por el sistema un tanto brutal de retener un carácter de cada tres. Por último, coloca la versión “resumida” en un nuevo fichero, cuyo nombre es el mismo que el del fichero antiguo con .red detrás (por reducido). Las dos indicaciones del comienzo y final (argumentos en línea de ejecución y apéndice al nombre del fichero) son de uso bastante general. E1 programa en sí, por el contrario, tiene una utilidad muy limitada, aunque, como vere mos, se le pueden encontrar algunas aplicaciones. / * reduce su fichero a la tercera parte! */ #include main(argc, argv) int argc; char *argv[]; { FILE *in, *out; /* declara dos punteros FILE */ int ch; static char nombre[20] ; /* para guardar fichero salida * / int cont = 0; if (argc < 2) /* comprueba que hay fichero de entrada */ printf("Lo siento, falta nombre fichero como argumento.\n”); else { if ( (in = fopen(argv[1], "r")) != NULL)
Una vez escrito y compilado el programa, lo colocamos en un fichero lla mado reduce. Después lo aplicamos a un fichero, llamado mili, que contenía una única línea: Mi ayuno durara y deliran veteranos
La orden utilizada fue reduce mili
y la salida se envió a un fichero llamado mili.red, que acabó conteniendo Manda dinero
¡Caramba! ¡Qué suerte más extraordinaria! Un fichero seleccionado al azar produjo un mensaje inteligible. A continuación comentaremos algunos aspectos del programa. Recuerde que argc es el número de argumentos, incluyendo el propio nom bre del fichero del programa. Si tenemos esto en cuenta, y el sistema operati vo lo permite, argv[0] representará el nombre del programa, en nuestro caso reduce. Por tanto, argv[l] contendrá el primer argumento, que en nuestro ejemplo era mili. Como argv[l] es por sí mismo un puntero a una tira de ca racteres, no lo hemos colocado entre comillas en la llamada función. También usamos argc para comprobar si existe un argumento. Si se in troducen más argumentos detrás, quedan ignorados; sin embargo, se puede añadir fácilmente otro bucle al programa, de forma que éste pudiera utilizar sucesivamente nombres de ficheros y aplicarles la reducción correspondiente por turno. 429
A diferencia de getc( ) y putc( ), estas dos funciones toman el puntero co mo primer argumento. Las dos restantes lo toman en último lugar. A fin de construir el nombre del fichero de salida, usamos strcpy( ) para copiar el nombre mili en el array nombre. Seguidamente utilizamos la fun ción strcat( ), para unir este nombre con .red. En el programa tenemos dos ficheros abiertos simultáneamente; por tan to, deberemos declarar dos punteros de tipo ‘FILE’. Observe que cada fiche ro se abre y cierra independientemente del otro. Existen límites en el número de ficheros que se pueden tener abiertos a un tiempo; el límite depende del sistema, pero generalmente está en el rango de 10 a 20. Se puede emplear el mismo puntero para diferentes ficheros, siempre que no haya dos ficheros con el mismo puntero abiertos a la vez. Tampoco estamos limitados simplemente a getc( ) y putc( ) en ficheros E/S. Estudiaremos ahora otras posibilidades.
Fiche ro E/ S:
Todas las funciones de E/S que hemos utilizado en capítulos anteriores tienen su equivalente en ficheros de E/S. La diferencia fundamental es que necesitaremos un puntero FILE para indicar a las nuevas funciones el fichero con el que tienen que trabajar. Al igual que sucedía con getc( ) y putc( ), estas funciones se usan después de que fopen( ) ha abierto el fichero, y antes de que fclose( ) lo cierre. Las funciones fprintf( ) y fscanf( )
Estas dos funciones de E/S se comportan exactamente igual que printf( ) y scanf( ), excepto que requieren un argumento adicional para apuntar al fichero correspondiente. Este argumento está en primer lugar en la lista. Vea mos un ejemplo. /* formato para usar fprintf() y fscanf() */ #include main() { FILE *fi; int edad;
}
Esta función utiliza tres argumentos, en lugar de uno como gets( ). Un ejemplo podría ser: /* lee de un fichero una linea cada vez */ #include #define MAXLIN 80 main() {
FILE *f1; char *tira[MAXLIN];
}
fprintf( ), fscanf( ), fgets( ) y fputsf( )
fi = fopen("pedro","r"); /* modo lectura fscanf(fi, "%d", &edad); /* fi apunta a pedro fclose(fi) ; fi = fopen("datos", "a"); /* modo apendice fprintf(fi, "pedro tiene %d. \n", edad); /* fi apunta a datos
La función fgets( )
f1 = fopen("cuento", "r"); while (fgets(tira, MAXLIN, f1) != NULL) puts(tira);
El primero de los tres argumentos de fgets( ) es un puntero al lugar de destino de la línea que se va a leer; en nuestro caso hemos colocado la entra da en un array de char llamado tira. El segundo argumento limita la longitud de la tira que se está leyendo. La función se detiene cuando se lee un carácter nuevalínea o MAXLIN—1 caracteres, lo que suceda primero. En cualquiera de los casos, se añade un carácter nulo (‘\0’) al final de la tira. El tercer argumento es, por supuesto, un puntero al fichero que se está leyendo. Una diferencia entre gets( ) y fgets( ) es que la primera sustituye el carácter nuevalínea con el ‘ \ 0’, en tanto que fgets( ) mantiene el carácter nuevalínea. Al igual que gets( ), fgets( ) devuelve un valor NULL cuando encuentra un carácter EOF. Esto permite comprobar, como hemos hecho, si se ha lle gado al final del fichero. La función fputs( )
Esta función es bastante semejante a puts( ). La sentencia */ */ */
*/
fclose(fi);
Observe que podemos usar fi en dos ficheros diferentes porque hemos ce rrado el primero antes de abrir el segundo.
fputs(“Por fin haces algo bien.", puntfich);
envía la tira de caracteres “Por fin haces algo bien.’’ al fichero apuntado por puntfich, un puntero de tipo FILE. Naturalmente, este fichero deberá haber sido abierto con anterioridad por fopen( ). La forma más general de uso es control = fputs(puntero tira, puntero fichero):
431
{
donde control es un entero que toma el valor EOF si fputs( ) encuentra un EOF o un error. Al igual que puts( ), esta función no copia el ‘ \0’ del final de la tira. Sin embargo, a diferencia de la anterior función, fputs( ) no añade un carác ter nuevalínea en la salida. Estas seis funciones de E/S que acabamos de discutir nos otorgan facili dades más que suficientes para la lectura y escritura de ficheros de texto. Que da aún otra función que puede ser de gran utilidad, y que pasamos a discutir a continuación.
Acceso aleatorio: fseek( ) La función fseek( ) permite tratar los ficheros como arrays, moviéndose directamente a un byte determinado del fichero abierto por fopen( ). Pre sentamos a continuación un ejemplo directo que muestra su funcionamien to. Hemos tomado prestado, de ejemplos anteriores, el párrafo de argumen tos en línea de ejecución destinado a conseguir el nombre del fichero sobre el que se va a trabajar. Observe que fseek( ) tiene tres argumentos y devuelve un valor int. /* usa fseek() para imprimir el contenido de un fichero */
#include main(numero, nombres) int numero; char *nombres[] ;
/* no hay por que usar argc y argv */
FILE *fp; long offset = 0L;
/* observe tipo long */
if (numero < 2) puts("Necesito un fichero como argumento."); else { if ( (fp = fopen(nombres[1] , "r")) == 0) printf("No puedo abrir %s.\n", nombres[1]) ;else
{ while (fseek(fp,offset++,0) putchar(getc(fp)); fclose(fp); }
== 0)
} } El primero de los tres argumentos de fseek( ) es un puntero FILE al fi
chero que es objeto de la búsqueda. Dicho fichero deberá haber sido abierto previamente con un fopen( ). El segundo argumento se denomina el “offset” (por ello utilizamos este nombre para la variable). Este argumento indica la distancia a que debemos movernos desde el punto de comienzo (véase más adelante); deberá declarar se como tipo long. Puede ser positivo (movimiento hacia adelante) o negati vo (movimiento hacia atrás). El tercer argumento es el modo que identifica el punto de referencia para el offset: MODO
0 1 2
EL OFFSET SE MIDE DESDE
el comienzo de posición actual fin de fichero
fichero
El valor devuelto por el return de fseek( ) es 0, siempre que haya funcio
nado todo correctamente; si aparece un error, como intentar avanzar más allá de los límites del fichero, el valor devuelto es —1. Ya podemos explicar ahora nuestro pequeño bucle: while (fseek
Al inicializarse offset a 0, la primera vez que se ejecuta el bucle tendre
mos la expresión fseek(fp,0L,0)
lo que significa literalmente: ir al fichero apuntado por fp y localizar el byte que está a 0 byte de distancia del comienzo. O, lo que es lo mismo, ir al pri-
433
mer byte. La función putchar( ), a continuación, imprimirá el contenido de dicho byte. La siguiente vez que se ejecute el bucle, offset se habrá incremen tado en 1L, imprimiéndose, por tanto, el siguiente byte. En esencia, la varia ble offset está actuando como un subíndice de los elementos del fichero. El proceso continúa hasta que offset intenta llevar a fseek( ) más allá del fin de fichero. En ese momento, fseek( ) devuelve un valor —1, y el bucle se de tiene. Este último ejemplo tiene un carácter puramente didáctico; no necesita mos fseek( ) en este caso, ya que con getc( ) podríamos haber rastreado el fichero byte a byte de igual forma; de hecho, fseek( ) indica a getc( ) que busque justamente donde esta última iba ya a buscar. En la figura 15.2 presentamos un ejemplo menos corriente. (La idea ori ginal es de una obra de William Shakespeare, La Duodécima Noche.) / * alterna impresion a izquierda y derecha */ #include main(numero, nombres) /* no hay por que usar argc y argv */ int numero; char *nombres[] ;
{
FILE *fp; long offset = 0L; if (numero < 2) puts("Necesito un fichero como argumento."); else { if ( (fp = fopen(nombres[1], "r")) == 0) printf("No puedo abrir %s.\n", nombres[1] ) ; else {
while (fseek(fp, offset++, 0) == 0) {
putchar(getc(fp)); if (fseek(fp,-(offset + 3), 2) == 0) putchar(getc(fp));
}
Nuestro programa imprime el primer carácter del fichero; a continuación, el último; más tarde, el segundo; después, el penúltimo, etc. Hemos utiliza do el mismo programa anterior, añadiendo las siguientes líneas: if (fseek(fp,-(offset + 3), 2) == 0) putchar(getc(fp));
El modo 2 significa que contamos posiciones desde el final del fichero. El signo negativo indica que se debe contar hacia atrás. El +3 figura con el fin de que se empiece a trabajar con el último carácter «normal» del fiche ro, evitando algunos caracteres nuevalínea y EOF que se colocan en el final real del fichero. (El valor exacto de este ajuste depende del sistema. Nuestros ficheros finalizan con 2 caracteres nuevalínea seguidos por 2 EOF, por lo que hemos retrocedido hasta pasarlos.) Esta es la parte del programa que alterna la escritura de derecha a izquierda y de izquierda a derecha. Debemos mencionar que algunos sistemas no acep tan el modo 2 en fseek( ). Bien, con esto pensamos que ya hemos digerido bastantes ficheros por el momento. Cerraremos el tema, y nos dedicaremos a otra sección de la bi blioteca C.
Comprobación y conversión de caracteres El fichero de encabezamiento ctype.h define varias funciones macros que comprueban la clase a que pertenecen distintos caracteres. Por ejemplo, la función isalpha(c) devuelve un valor no 0 (cierto) si c es un carácter alfabéti co, mientras que devuelve un 0 (falso) si el carácter no es alfabético. Por tanto,
fclose(fp);
}
}
}
isalpha('S') ! = 0 ,
Figura 15.2
Programa con impresión alternante a izquierda y derecha
pero isalpha ( ' # ' ) = = 0
A continuación presentamos una lista con las funciones que se suelen en contrar más comúnmente en dicho fichero. En cada caso, la función devuel ve un valor distinto de 0, si c pertenece a la clase comprobada, y 0, si no es así. Función
La aplicación de este programa a un fichero que contenga el nombre «Ma rinero» produce este curioso resultado:
isalpha(c) isdigit(c)
MoarreinnierraoM
islower ( c ) isspace(c) isupper(c)
el test decide si c es
alfabético dígito minúscula espacio en blanco (espacio, tabulado o nuevalínea) mayúscula 435
En su sistema puede que existan funciones adicionales como Función isalnum(c) isascii(c)
iscntrl(c)
ispunct(c)
el test decide si c es
alfanumérico (alfabético o dígito) ASCII (0-127) carácter de control signo de puntuación
printf("De acuerdo, va a minusculas.\n"); return(MINUS) ;
}
prepfich(nombre1, nombre2) char *nombrel, *nombre2;
Existen dos funciones más que realizan conversaciones: toupper(c)
tolower(c)
convierte c a mayúsculas convierte c a minúsculas
En algunos sistemas, la conversión se intenta únicamente cuando el ca rácter no es del tipo de letra solicitado. Sin embargo, conviene asegurarse primero de la “mayusculalidad” o “minusculalidad” del carácter. En la figura 15.3 hay un programa que emplea algunas de estas funciones para convertir un fichero completamente a letras mayúsculas o a letras mi núsculas, según se desee. Con el fin de variar un poco, usaremos esta vez un sistema interactivo para enviar la información requerida por el programa en lugar de los argumentos en línea de ejecución. /*
}
}
printf(" Que fichero desea convertir?\n"); gets (nombrel) ; printf( "Fichero elegido: \"%s\". \n", nombrel); printf("Nombre del nuevo fichero convertido\n"); while (strcmp(gets(nombre2) nombrel) == NULL) printf("El nombre ha de ser diferente.\n"); printf("El fichero de salida sera \"%s\".\n", nombre2);
conv(nombrel, nombre2, crit) char *nombrel, *nombre2; int crit; { int ch; FILE *fl, *f2; if ( (f1 = fopen(nombre1, "r")) == NULL) printf("Lo siento, no puedo abrir %s. Adios.\n", nombrel); else { puts("Alla vamos!"); f2 = fopen(nombre2, "w"); while ( (ch = getc(fl)) != EOF)
conversion a MAYUSCULAS o minusculas */
#include
{
#include /* incluye un fichero de macros */
if (crit == MAYUS) ch = islower(ch) ? toupper(ch) : ch; else ch = isupper(ch) ? tolower(ch) : ch; pute(ch,f2);
#define MAYUS 1 #define MINUS 0 main()
{
}
int crit; /* sera despues MAYUS o MINUS */ char fich1[14],fich2[14] /* nombres entrada y salida */
}
crit = escoge(); /* escoge mayuscula o minuscula */ prepfich(fichl, fich2);/* toma los nombres de ficheros */ conv(fich1, fich2,crit); / * realiza la conversion */
escoge()
{
fclose(f2); fclose(f1) ; puts("Terminado!");
}
}
Figura 15.3
Programa convertidor de tipos de letras
int ch; printf("Este programa convierte un fichero completo a\n") ) printf("MAYUSCULAS o minusculas. Introduzca A si desea\n"); printf("mayusculas o I para minusculas.\n"); while ((ch = getchar()) != 'A' && ch != 'a' && ch != 'I' && ch != 'i') printf("Introduzca una A o una I.\n"); while (getchar() != '\n') ; /* limpia entrada */ if ( ch == 'A' || ch == 'a')
{
printf("De acuerdo, va a mayusculas.\n"); return(MAYUS) ;
}
else
{
Hemos dividido el programa en tres partes: la obtención de la decisión de usuario sobre el tipo de letra, la preparación de nombres para los ficheros de entrada y salida y la propia conversión. Para no oxidarnos, hemos desa rrollado una función distinta para cada parte. La función escoge( ) es bastante directa, quizá con la excepción del bucle while (getchar() != '\n' )
Este bucle está incluido para resolver un problema que ya apareció en el capítulo 14. Cuando el usuario responde a la pregunta sobre mayúsculas y 437
char ch; static char numero[TAM]; int valor; int digito = SI; int cont = 0;
minúsculas, deberá pulsar una letra, por ejemplo la A, y a continuación la tecla [enter], que transmite un carácter ‘\n’. La función inicial getchar( ) recoge la A, pero deja el carácter ‘ \n’ en el buffer de entrada hasta la si guiente lectura. La función gets( ), que viene a continuación dentro de prepfich( ), interpretará este carácter como una línea vacía. Por ello empleamos el pequeño bucle while para deshacernos del carácter nuevalínea. En reali dad, un simple getchar( ); hubiera valido, suponiendo que el usuario pulsara [enter] inmediatamente después de A; sin embargo, nuestra versión permite también que se pulsen espacios entre la letra y el [enter]. La función prepfich( ) contiene muy pocas novedades. Observe que he mos previsto que el usuario intente utilizar el mismo nombre como fichero de entrada y de salida. La versión estándar de fopen( ) no permite leer y es cribir en el mismo fichero al mismo tiempo. La función conv( ) es una función de copia con un convertidor de tipos añadido. El valor de crit decide qué conversión se va a realizar. La tarea la pueden llevar a cabo sentencias condicionales sencillas, como
puts("Introduzca un entero.\n"); gets(numero); if (numero[TAM-1] != '\0') puts("Demasiadas cifras; me ha fundido los bits.");
exit(l) ;
} while ( (ch = numero[cont]) != '\0' && dígito == SI) if (!issign(ch) && !isdigit(ch) && !isspace(ch)) digito = NO; if (digito == SI)
{ valor = atoi(numero) ; printf("El numero leido es %d. \n", valor);
} else
printf("Eso no tiene pinta de entero.\n");
} ch = islower(ch) ? toupper(ch) : ch;
Con ello se comprueba si ch es minúscula; si lo es, se convierte en mayús cula; en caso contrario, se deja como está. Las macros de ctype.h contienen herramientas útiles, adecuadas para una programación más agradable. Nos ocuparemos ahora de algunas funciones de conversión de naturaleza más ambiciosa.
Conversiones de tiras de caracteres: atoi( ), atof( ) El uso de scanf( ) para leer valores numéricos no es, ni con mucho, la política más segura. Scanf( ) depende demasiado de los errores de usuario en la introducción de los números. Muchos programadores prefieren leer in cluso los datos numéricos como tiras y convertirlos de tira al valor numérico apropiado. Para ello se dispone de las dos funciones atoi( ) y atof( ). La pri mera convierte una tira en entero, mientras que la segunda convierte la tira en un número en punto flotante. En la figura 15.4 presentamos un ejemplo de utilización: /* utilizacion de atoi() */ #include #define issign(c) ( ((c) == '-' || (c) == '+') ? (1) : (0) ) #define TAM 10 #define SI 1 #define NO 0 main()
{
Figura 15.4 Programa que utiliza a atoi( )
Hemos incluido algunas comprobaciones de errores. Primero comproba mos si la tira de entrada es demasiado larga para el array de destino. Como el array número es static char, está inicializado a nulos. Si el último miembro del array no es un nulo, es señal evidente de que algo ha ido mal, y el progra ma finaliza. Aquí empleamos la función de biblioteca exit( ), la cual permite abandonar el programa; ahondaremos más en esta función dentro de un mo mento. Seguidamente se comprueba si la tira contiene caracteres extraños, es de cir, algo que no sean espacios, números o signos algebraicos. Con esta pre caución se rechazan tiras como “tres” o “1.2E2”. Se aceptaría, sin embar go, algo como “3—4 + 2”, pero ya dará cuenta de ello atoi( ). Recorde mos que ! es un operador de negación; por tanto, !isdigit(c) significa “c no es un dígito”. La línea valor = atoi(numero) ;
demuestra cómo se utiliza atoi( ). Su argumento es un puntero a una tira; en este caso hemos usado el nombre de array número. La función devuelve un valor int para la tira. Así, “1234”, que es una tira de cuatro caracteres, se transforma en 1234, un valor entero simple. La función atoi( ) ignora los blancos anteriores al número; procesa un signo algebraico de comienzo, si existe, y a continuación acepta dígitos hasta el primer carácter que no lo sea; por consiguiente, nuestro anterior ejemplo 439
“3—4 + 2” se convertiría en el valor 3. Observe en las cuestiones del final del capítulo una posible implemantación. La función atof( ) realiza una tarea similar en números del punto flotan te. Devuelve un tipo double, de forma que debe ser declarada así en los pro gramas que la utilicen. Las versiones sencillas de atof( ) son capaces de manejar números como 10.2, 46 y —124.26. Existen versiones más avanzadas que aceptan, además, notación exponencial, es decir, números como 1.25E—13. En su sistema puede que existan otras funciones que trabajan en sentido contrario. Así, una función itoa( ) convertiría un entero en tira, mientras que ftoa( ) convertiría un número de punto flotante en tira.
Salida: exit( ) La función exit( ) proporciona una forma muy conveniente de abando nar un programa. A menudo se acostumbra a detener un programa cuando aparece un error; cuando se llama a la función exit( ) desde una función que, a su vez, ha sido llamada por el programa principal, se detiene el programa completo, y no simplemente la función. En el caso atoi( ), anterior el uso de exit( ), permite evitar la inclusión de una sentencia else extra que impidie se la intervención del resto del programa. Un gran favor que realiza exit( ) es cerrar todos los ficheros abiertos con fopen( ). Con ello se consigue que el final del programa sea mucho menos doloroso. El argumento de exit( ) es un número de código de error. En algunos sis temas este número puede traspasarse a otro programa cuando se llega a un exit en el programa de ejecución. La convención utilizada es que un 0 indica terminación normal, en tanto que el resto de valores indican un problema. Antes de que se nos olvide, tenemos otro asunto que tratar.
Pero el C va más allá. Permite asignar más memoria a medida que el programa se ejecuta. Supongamos, por ejemplo, que estamos escribiendo un programa interactivo, y no sabemos de antemano cuántas entradas vamos a realizar. Lo que se hace en un caso como éste es reservar, en principio, una cantidad de memoria razonable, y solicitar más en ejecución, si es necesario. En la figura 15.5 hay un ejemplo que utiliza la función malloc( ) para hacer esta tarea. Observe, además, el uso de los punteros en este programa. /* toma mas memoria si hace falta */ #include #define STOP "" /* signo para acabar la entrada #define BLOQUE 100 /* bytes de memoria #define LIM 40 / * longitud max linea entrada #define MAX 50 /* numero max lineas entrada #define DRAMA 20000 /* pausa dramatica de malloc() main() { char almacen[BL0QUE]; /* bloque original almacenamiento char sinfo[LIM]; /* receptor de entradas char *fin; /* puntero a final almacenamiento char *entradas[MAX]; /* punteros a comienzos de tiras int indice = 0; /* numero de lineas introducidas int cont = 0; /* contador char *malloc(); /* asignador de memoria
*/ */ */ */ */ */ */
entradas[0] = almacen; fin = entradas[0] + BLOQUE - 1; puts("Nombre algunas orquestas sinfonicas."); puts("Introduzca una cada vez; pulse [enter] a comienzo de"); puts<"linea para terminar. Adelante, estoy listo."); while (strcmp(fgets(sinfo, LIM, stdin), STOP) != 0 && indice < MAX) { if (strlen(sinfo) > fin - entradas[indice]) { /* esta parte se ejecuta si no hay suficiente memoria */ puts(“Espere un momento, tengo que buscar mas memoria."); entradas[indice] = malloc(BLOQUE); fin = entradas[indice] + BLOQUE - 1; for (cont = 0; cont < DRAMA; cont++) ; puts("Ya he encontrado otro trozo!"); } strcpy(entradas[indice], sinfo); entradas[indice + 1] = entradas[indice] + strlen(sinfo) + 1; if (++indice < MAX) printf("Ya tenemos %d. Continue si lo desea. \n", indice); } puts("De acuerdo. Tenemos:"); for (cont = 0; cont < indice; cont++) puts(entradas[cont]);
Asignación de memoria: malloc( ) y calloc( ) Su programa deberá disponer de memoria suficiente para almacenar los datos que utilice. Parte de esta “asignación de memoria” se realiza automá ticamente; por ejemplo, podemos declarar: char sitio[] = "Monasterio de Piedra";
lo que garantiza disponer de memoria suficiente para almacenar esa tira. También podemos ser más explícitos y solicitar una cierta cantidad de me moria:
*/ */ */ */ */
} Figura 15.5 Programa para añadir memoria según haga falta
int platos[100];
Esta declaración prepara 100 localizaciones de memoria, cada una de ellas suficiente para albergar un valor int.
Una salida de este programa podría ser: Nombre algunas orquestas sinfonicas. Introduzca una cada vez; pulse [enter] a comienzo de 441
linea para terminar. Adelante, estoy listo. Filarmonica de Viena Ya tenemos 1.. Continue si lo desea. Sinfonica de San francisco Ya tenemos 2. Continue si lo desea. Filarmonica de Berlin Ya tenemos 3. Continue si lo desea. Sinfonica de Radiotelevision Ya tenemos 4. Continue si lo desea. Sinfonica de Londres Ya tenemos 5. Continue si lo desea. La Concertgebouw Espere un momento, tengo que buscar mas memoria. Ya he encontrado otro trozo! Ya tenemos Continue si lo desea. Sinfonica de Chicago Ya tenemos 7. Continúe si lo desea. De acuerdo. Tenemos: Filarmonica de Viena Sinfonica de San Francisco Filarmonica de Berlin Sinfonica de Radiotelevision Sinfonica de Londres La Concertgebouw Sinfonica de Chicago
En primer lugar, observemos lo que hace malloc( ). Toma un argumento entero sin signo que representa el número de bytes de memoria requeridos. Así, malloc(BLOQUE) solicita 100 bytes de memoria. La función devuelve un puntero char al comienzo del nuevo bloque de memoria. Hemos usado la declaración char
*mallo c ( )
;
para advertir al compilador de que malloc( ) devuelve un puntero char. En tonces asignamos el valor de este puntero a entradas[índice] con la sentencia entradas[indice] = malloc[BLOQUE);
Bien, observemos ahora el funcionamiento del programa. Se trata de al macenar las tiras de entrada todas ellas en un gran array llamado almacén. Hacemos entradas[0] igual al punto de comienzo de la primera tira: entradas[l], al punto de comienzo de la segunda, y así sucesivamente. Como etapa inter media, el programa lee la tira dentro del array sinfo. Utilizamos fgets( ) en lugar de gets( ), para poder limitar la tira de entrada hasta ajustarse a sinfo.
F igu ra 15.6
Sinfónicas consecutivas almacenadas en el almacén
Antes de copiar sinfo en el almacén, deberemos comprobar si tenemos espacio suficiente. El puntero fin apunta al final del almacenamiento, mien tras que el valor que tiene entradas[índice] en ese momento corresponde al comienzo de la parte de almacenamiento sin utilizar. Si comparamos la dife rencia entre estos dos punteros con la longitud de sinfo podremos decidir si queda espacio suficiente. Si no queda espacio, llamaremos a malloc( ) para preparar un nuevo blo que de almacenamiento. Entonces hacemos que entradas[índice] apunte al comienzo del nuevo bloque y que fin apunte al final del mismo. Observe que no disponemos de un nombre para este nuevo espacio de almacenamiento; no es, por ejemplo, una extensión de almacén. La única identificación de que disponemos son los punteros que apuntan a la nueva área. Conforme el programa se va ejecutando, cada nueva tira de caracteres queda apuntada por un miembro del array de punteros entradas. Algunas tiras están en almacén; otras, en una o más áreas nuevas de almacenamiento. Sin embargo, en tanto en cuanto disponemos de los punteros, podemos acce der a las tiras, como queda demostrado por la salida del programa. Este es el modo en que se usa malloc( ). Supongamos que ahora desea mos memoria de tipo int, no de tipo char. Se puede usar también malloc( ) de la siguiente forma: char *malloc();/*se sigue declarando como puntero a char */ int *nuevo ; nuevo = (int *)malloc(100);/ * se usa operador de moldeado */
Una vez más hemos reservado 100 bytes de memoria. El operador de mol deado convierte el valor devuelto de puntero char a puntero int. En nuestro sistema, int ocupa dos bytes de memoria, lo que significa que nuevo + 1 in crementará el puntero en dos bytes, lo cual le permite apuntar al siguiente entero. Evidentemente, los 100 bytes reservados podrán utilizarse como al macén de 50 números enteros. Otra opción para reservar memoria en la función calloc( ). Un ejemplo típico podría ser: char *calloc(); long *nuevo; nuevo = (long *)calloc(100, sizeof(long));
Al igual que malloc( ), calloc( ) devuelve un puntero a char. Se debe uti lizar el operador de moldeado si se desea almacenar un tipo diferente. Esta nueva función tiene dos argumentos, ambos enteros sin signo. El primer ar gumento es el número de células de memoria deseado; el segundo es el tama ño de cada célula en bytes. En nuestro caso, long utiliza cuatro bytes, por lo que esta instrucción prepara 100 unidades de cuatro bytes cada una, utili zando en total 400 bytes. 443
Hasta ahora hemos aprendido Hemos usado sizeof (long) en lugar de 4, con el fin de hacer el código más transportable; este fragmento funcionará incluso en sistemas en que long tenga un tamaño diferente del nuestro. La función calloc( ) tiene una propiedad adicional: hace todos los conte nidos del bloque iguales a 0. En la librería C de su sistema encontrará probablemente otras funciones de gestión de memoria; le aconsejamos que revise los manuales correspon dientes.
Qué es una biblioteca C y cómo utilizarla Cómo abrir y cerrar ficheros de texto: fopen( ) y fclose( ) Qué es un tipo FILE Cómo leer y escribir en ficheros: getc( ), putc( ), fgets( ), fscanf( ), fprintf( )
Cómo comprobar clases de caracteres: isdigitf ), isalpha( ), etc. Cómo convertir tiras en números: atoi( ), atof( ) Cómo hacer una salida rápida: exit( ) Cómo asignar memoria: malloc( ), calloc( )
Funciones de biblioteca La mayor parte de las bibliotecas poseen algunas otras funciones en las áreas que hemos repasado. Además de funciones de asignación de memoria, existen funciones para liberar memoria que ya no se va a volver a emplear. Pueden existir también otras funciones de tratamiento de tiras, quizá funcio nes que buscan un carácter concreto o grupo de caracteres dentro de una tira. Otras funciones de fichero podrían incluir open( ), close( ), create( ), lseek( ), read( ) y write( ). Todas ellas cumplen las mismas tareas que las funciones que hemos discutido, pero a un nivel más básico. De hecho, las funciones del tipo fopen( ) están escritas apoyándose en estas funciones más básicas. Su utilización suele ser más molesta, pero permite el tratamiento de ficheros binarios además de los ficheros de texto. En su sistema puede existir también una biblioteca matemática. Típica mente, tal biblioteca contiene una función raíz cuadrada, una función po tencia, una función exponencial, varias funciones trigonométricas y una fun ción para generación de números aleatorios. Le llevará algún tiempo explorar el total de funciones que ofrece su siste ma. Si no encuentra lo que busca, fabríquese sus propias funciones. Recuer de que esta filosofía es parte integrante del C. Y, evidentemente, si cree que puede hacerlo mejor, por ejemplo, en una función de entrada, ¡hágalo! Conforme vaya refinando y puliendo su técnica de programación, pasará de escribir programas en C a realizar obras de arte C.
Cuestiones y respuestas Cuestiones
1. ¿Qué errores contiene este programa? main()
{ int *fp; int k; fp = fopen("galletas"); for (k = O; k < 30; k++) fputs(fp, "Marta come galletas."); fclose("galletas");
} 2. ¿Qué se supone que hace el siguiente programa? #include #include (ctype.h> main(argc,argv) int argc; char *argv[];
{ int ch; FILE *fp;
Conclusión Ha sido un camino bastante largo desde que se comenzó el libro. Hemos encontrado en él la mayoría de características básicas del lenguaje C. Las omi siones más notables —operaciones con bit y extensiones UNIX 7— se cubren brevemente en el apéndice F. Hemos visto la plétora de operadores del C; admirado su enorme variedad de tipos de datos básicos y derivados; obser vado sus inteligentes estructuras de control, y tanteado su poderoso sistema de punteros. Tenemos la esperanza de haberle ayudado a prepararse para uti lizar C para sus propios fines. Siéntese delante del teclado, y ¡buena suerte y buenos programas!
if ((fp = fopen(argv[1] , "r")) == NULL) exit(1) ; while ((ch = getc(fp) != EOF) if (isdigit(ch)) putchar(ch); fclose(fp);
} 3. ¿Existe algún problema en utilizar expresiones como isalpha(c[i]), cuando c es un array de char? ¿Y una expresión como isalpha(c[i + + ])? 4. Utilice las funciones de clasificación de caracteres para preparar una implementación de atoi( ). 5. ¿Cómo podría asignar espacio extra para almacenar un array de estructuras? 445
Respuestas 1. Debe haber un #include < stdio.h > para las decisiones de fichero. Se declara fp como puntero file: FILE *fp;. La función fopen( ) requiere un modo: fopen(“galletas", “w”) o quizá el modo “a”. Se debe cambiar el orden de los argumentos de fputs( ). La función fclose( ) necesita un fichero file, no el nombre del fichero: fclose(fp); 2. Abriría el fichero dado como argumento en línea de ejecución e imprimiría todos los nú meros del fichero. Debería comprobar (pero no lo hace) si existe argumento en línea de ejecución. 3. La primera expresión es correcta, porque c[i] tiene un valor char. La segunda expresión no molestará al operador, pero puede dar resultados inesperados. La razón es que isalpha( ) es una macro, en la que con toda probabilidad aparece su argumento dos veces (com probación de minúsculas y comprobación de mayúsculas), lo cual produciría dos incre mentos en i. Es aconsejable evitar el uso de operadores incremento en los argumentos de una llamada función macro. 4. #include #include #define issign(c) ( ((c) == '-' || (c) == '+') ? (1) : (0) ) atoi(s) char *s; {
Apéndice A Lecturas adicionales
int i = 0; int n, signo; while (isspace(s[i]) ) i++; / * salta espacios en blanco * / signo = 1; if (issign(s[i]) / * maneja signo opcional */ signo = (s[i++] == '+' ) ? 1 : -1; for (n = 0; isdigit(s[i]); i++) n = 10*n + s[i] - '0'; return(signo*n);
Si desea aprender más acerca de programación y de programación en C, encontrará útiles las siguientes referencias. Por lo que sabemos, el libro que está en sus manos es el primer libro de C en español; por tanto, las referen cias de lenguaje C se refieren, necesariamente, a lenguas extranjeras.
} 5. Supongamos que vino es la etiqueta de la estructura. Las siguientes sentencias, colocadas adecuadamente en el programa, cumplirán la tarea solicitada. struct vino *ptrvino ; char *calloc() ; ptrvino = (struct vino *) calloc(100, sizeof(struct vino));
Ejercicios 1. Escriba un programa de copia de ficheros que utilice el nombre original del fiche ro y el nombre del fichero copiado como argumento en línea de ejecución. 2. Escriba un programa que tome todos los ficheros dados en una serie de argumen tos de línea de ejecución, y los imprima uno tras otro en pantalla. Utilice arge para establecer un bucle. 3. Modifique nuestro programa de inventario de libros del capítulo 14 de tal forma que la información se pueda añadir a un fichero llamado mislibros. 4. Utilizando gets( ) y atoi( ), construya el equivalente a nuestra función getint( ) del capítulo 10. 5. Escriba de nuevo el programa contador de palabras del capítulo 7 utilizando macros de ctype.h y un argumento en línea de ejecución con el fichero a procesar.
Lenguaje C Kernighan, Brian W., y Ritchie, Dennis M.: The C Programming Language.
Prentice-Hall, 1978. Este libro es la mayor autoridad en C, y el primer libro sobre el tema. Obsérvese que uno de los autores, Dennis Ritchie, es el creador del C. Está incluida en él la definición oficial del C y, además, un gran número de ejem plos interesantes. Sin embargo, supone que el lector está familiarizado con programación de sistemas. Feüer, Alan R.: The C Puzzle Book. Prentice-Hall, 1982. Ritchie, D. M.; Johnson S. C.; Lesk, M. E. y Kernighan, B. W.: «The C
Programming Languaje», en The Bell System Technical Journal, v. 57, n. 6, julio-agosto 1978. Este artículo discute la historia del C y da una visión de conjunto de las características de diseño. BYTE: v. 8, n. 8, agosto 1983.
Este número de la revista B YTE está dedicado al C. Incluye artículos que discuten su historia, filosofía y utilidad. Se estudian y evalúan veinte compi447
Apéndice B Palabras clave en C
ladores de C para microprocesadores. También se incluye una bibliografía extensa y puesta al día de libros y artículos sobre C. Cada referencia biblio gráfica incluye un corto resumen del libro o artículo en cuestión.
Programación Wirth, N.: Algoritmos + Estructuras de Datos = Programas. Ed. del Casti
llo, 1983. Kernighan, Brian W., y Plauger, P. J.: The Elements of Programming Style,
2.a ed., McGraw-Hill, 1978. — Software Tools. Addison-Wesley, 1976. Ghezzi, C. y Jazayeri, M.: Programming Language Concepts, John Wiley
& Sons, 1982. Schneider, G. M., y Brueli, S. C.: Advanced Programming and Problems
Solving with Pascal. J. Wiley & Sons, 1981. El primero es uno de los pocos libros de programación racional traduci dos al castellano. Su autor es el inventor del PASCAL. En la misma línea, tam bién basado en PASCAL, aunque de mayor nivel, es el último libro de la lista.
Las palabras clave son las palabras empleadas en un lenguaje para expre sar las acciones del mismo. Las palabras clave del C son reservadas; es decir, no se pueden utilizar para otros fines, como nombres de variables.
Palabras clave de control de programas
Sistema operativo UNIX Waite, Mitchell; Martin, Don, y Prata, Stephen: UNIX sistema V. Anaya
Multimedia, 1986. Este libro facilita una introducción sencilla al sistema operativo UNIX, incluyendo algunas poderosas mejoras de Berkeley.
Bucles for while do Decisión y elección if else switch case default Saltos break continue goto Tipos de datos char int short long unsigned float double struct union typedef Modos de almacenamiento auto extern register static
449
Miscelánea return sizeof Aún no implementado entry Disponible únicamente en algunos sistemas
Apéndice C Operadores C
asm endasm fortran enum
El C está lleno de operadores. Presentamos aquí una tabla de los mismos indicando el rango de prioridad de cada uno, y cómo se ejecutan. A conti nuación comentaremos brevemente los operadores, con excepción de los ope radores de bit, que se discuten en el apéndice F.
451
IV.
La acción de estos operadores es la siguiente: I.
Operadores aritméticos
+ — — * /
Suma los valores situados a su derecha y a su izquierda. Resta el valor de su derecha del valor de su izquierda. Como operador unario, cambia el signo del valor situado a su derecha. Multiplica el valor de su derecha por el valor de su izquierda. Divide el valor situado a su izquierda por el valor situado a su derecha. Cuan do los dos operandos son enteros, la respuesta se trunca. % Proporciona el resto de la división del valor de la izquierda por el valor de la derecha (sólo enteros). + + Suma 1 al valor de la variable situada a su izquierda (modo prefijo) o de la variable situada a su derecha (modo sufijo). --Igual que + +, pero restando 1.
II.
Los operadores lógicos utilizan normalmente expresiones de relación co mo operandos. El operador ! toma un operando situado a su derecha; el res to toma dos: uno a su derecha y otro a su izquierda. && AND Lógico: la expresión combinada es cierta si ambos operandos lo son, y falsa en cualquier otro caso. || OR Lógico: la expresión combinada es cierta si uno o ambos operandos lo son, y falsa en cualquier otro caso. ! NOT Lógico: la expresión es cierta si el operando es falso, y viceversa. V.
nana = 22; ptr = &nana; /* puntero a nana */ val = *ptr;
= Asigna el valor de su derecha a la variable de su izquierda.
+ = Suma la cantidad d a la variable i. - = Resta la cantidad d de la variable i. * = Multiplica la variable i por la variable d. / = Divide la variable i entre la cantidad d. % = Proporciona el resto de la división de la variable i por la cantidad d. Ejemplo: conejos *= 1.6; es lo mismo que conejos = conejos * 1.6;
III.
Operadores de relación
Cada uno de estos operadores compara el valor de su izquierda con el valor de su derecha. La expresión de relación formada por un operador y sus dos operandos toma el valor 1 si la expresión es cierta, y el valor 0 si es falsa. < menor que <= menor o igual a == igual a >= mayor o igual a > mayor que != distinto de
Operadores relacionados con punteros
& Operador dirección: cuando va seguido por el nombre de una variable, entre ga la dirección de dicha variable: &nana es la dirección de la variable nana * Operador de indirección: cuando va seguido por un puntero, entrega el valor almacenado en la dirección apuntada por él:
Operadores de asignación
Cada uno de los siguientes operadores actualiza la variable de su izquier da con el valor de su derecha utilizando la operación indicada. Usaremos d e i para derecha e izquierda.
Operadores lógicos
El efecto neto es asignar a val el valor 22. VI.
Operadores de estructuras y uniones
El operador de pertenencia (punto) se utiliza junto con el nombre de la estruc tura o unión, para especificar un miembro de las mismas. Si tenemos una estructura cuyo nombre es nombre, y miembro es un miembro especificado por el patrón de la estructura, nombre.miembro
identifica dicho miembro de la estructura. El operador de pertenencia pue de utilizarse de la misma forma en uniones. Ejemplo: struct { int codigo; float precio; } articulo;
articulo.codigo = 1265;
Con esto se asigna un valor al miembro código de la estructura artículo. - > El operador de pertenencia indirecto: se usa con un puntero estructura o unió para identificar un miembro de las mismas. Supongamos que ptrstr es un puntero a una estructura que contiene un miembro especificado en el patrón de estructura con el nombre miembro. En este caso ptrstr—>miembro
identifica al miembro correspondiente de la estructura apuntada. El opere dor de pertenencia indirecto puede utilizarse de igual forma con uniones
Ejemplo: struct { int codigo; float precio; } articulo, *ptrstr; ptrstr = &articulo; ptrstr-> codigo = 3451;
De este modo se asigna un valor al miembro código de la estructura artículo. Las tres expresiones siguientes son equivalentes: ptrstr->codigo
articulo.codigo
(*ptrstr).codigo
VII. Misceláneas
Devuelve el tamaño, en bytes, del operando situado a su derecha. El operan do puede ser un especificador de tipo, en cuyo caso se emplean paréntesis; por ejemplo, sizeof (float). Puede ser también el nombre de una variable concreta o de un array, en cuyo caso no se emplean paréntesis: sizeof foto. (tipo) Operador de moldeado: convierte el valor que vaya a continuación en el tipo especificado por la palabra clave encerrada entre los paréntesis. Por ejem plo, (float)9 convierte el entero 9 en el número de punto flotante 9.0. El operador coma une dos expresiones en una, garantizando que se evalúa en primer lugar la expresión situada a la izquierda; una aplicación típica es la inclusión de más información en la expresión de control de un bucle
Apéndice D Tipos de datos y modos de almacenamiento
sizeof
for:
for (chatos = 2, ronda = 0; ronda < 1000; chatos *= 2) ronda += chatos;
?:
El operador condicional: utiliza tres operandos, cada uno de los cuales es una expresión; se ordenan de la siguiente forma: expresión1 ? expresión2 : expresión 3
El valor de la expresión completa equivale al de la expresión2 si expresión1 es cierta, y al valor de expresión3 en caso contrario. Ejemplos: (5 (3 (a
> 3 ) ? 1 : 2 toma el valor 1 > 5 ) ? 1 : 2 toma el valor 2 > b ) ? a : b toma el valor mayor entre a y b.
Tipos de datos básicos Palabras clave: Los tipos de datos básicos se preparan utilizando las 7 pala bras clave siguientes: int, short, unsigned, char, float, double. Enteros con signo: Pueden ser valores positivos o negativos, int: Es el tipo de entero básico de un sistema dado, long o long int: Puede almacenar un entero que, como mínimo, es del tama ño del mayor int y, posiblemente, mayor que short o short int; el mayor entero short es menor o igual que el mayor int, pudiendo ser menor, long normalmente será mayor que short, e int será del mismo tamaño que uno de los dos. Por ejemplo, el IBM PC Lattice C tiene short e int de 16 bits y long de 32 bits. Todos estos datos dependen del sistema. Enteros sin signos: Estos enteros sólo pueden tomar valores positivos o 0. Con ello se extiende el rango del mayor valor positivo alcanzable. Utilice la palabra clave unsigned delante del tipo deseado: unsigned int, unsigned long, unsigned short. unsigned en solitario se considera unsigned int. Caracteres: Son símbolos tipográficos como A, & y + . Generalmente se al macenan en un byte de memoria cada uno. char: palabra clave para este tipo. Punto flotante: Pueden tener valores positivos o negativos, float: Tamaño básico de punto flotante para el sistema, double o long float: una unidad (posiblemente) mayor para almacenar nú meros en punto flotante. Puede permitir más cifras significativas y qui zá exponentes mayores.
MODO DE ALMACENAMIENTO PALABRA CLAVE DURACION ALCANCE externo extern permanente global (todos los fi
Como declarar una variable simple 1. Escoja el tipo que necesite. 2. Escoja un nombre para la variable. 3. Utilice el siguiente formato en una sentencia de declaración: especificador de tipo nombre de variable; El especificador de tipo se forma con una o más de las palabras clave de tipo. Algunos ejemplos: int eres; unsigned short presa;
4. Se puede declarar en la misma sentencia más de una variable del mismo
tipo, separando los nombres de variables por comas: char ch, init, os;
5. Se puede inicializar una variable en la propia sentencia de declaración: float masa = 6.0e24;
modos de almacenamiento I. II.
Palabras clave: auto, external, static, register Comentarios generales
El modo de almacenamiento de una variable determina el alcance de la misma y el tiempo que la variable perdura en el programa. El modo de alma cenamiento queda fijado por la posición en que se define la variable y por su palabra clave asociada. Las variables que se definen fuera de las funcio nes son externas (external), y tienen alcance global. Las variables declaradas dentro de una función son automáticas y locales, a menos que se utilice una palabra clave diferente. Las variables externas definidas antes de una fun ción son conocidas por la misma incluso si no se declaran explícitamente dentro de ella. III.
Propiedades
MODO DE ALMACENAMIENTO PALABRA CLAVE DURACION
automático registro estático
auto register static
temporal temporal permanente
ALCANCE local local local
externo estático
static
cheros) permanente global (un fichero)
Los modos situados por encima de la línea de puntos se declaran dentro de las funciones. Los modos situados debajo de la línea se definen fuera de la función.
Apéndice E Control de flujo en el programa En C existen diversas estructuras de control para guiar el flujo del pro grama. Se resumen aquí las sentencias de bucles (while, for y do while), las de ramificación (if, if else y switch) y las sentencias de salto (goto, break y continue).
La sentencia while Palabra clave: while Comentarios generales:
La sentencia while crea un bucle que se repite hasta que la expresión de test se vuelve falsa o 0. La sentencia while es un bucle con condición de en trada. La decisión de atravesar el bucle una vez más se realiza antes de atra vesarlo; por consiguiente, es posible ejecutar el bucle 0 veces. La parte de sentencia del formato puede ser una sentencia simple o compuesta. Formato:
while ( expresión ) sentencia
La porción sentencia se repite hasta que la expresión se vuelve falsa o 0. 459
Ejemplos: while (n++ < 100) printf(" %d %d\n”, n, 2*n + 1 ) ; while (ronda < 1000) { ronda = ronda + chatos; chatos = 2 * chatos;
}
de salida; la decisión de atravesar el bucle se realiza después de haberlo atra vesado. Así pues, el bucle debe ejecutarse por lo menos una vez. La parte sentencia del formato puede ser una sentencia simple o compuesta. Formato: do
sentencia while ( expresión );
La porción sentencia se repite hasta que la expresión se vuelve falsa o 0.
La sentencia for Palabra clave: for Comentarios generales:
La sentencia for utiliza tres expresiones de control, separadas por puntos y comas, para controlar el proceso de bucle. La expresión de inicialización se ejecuta una vez, antes de comenzar el bucle. Si la expresión de test es cierta (distinta de 0) el bucle se ejecuta una vez. A continuación se evalúa la expre sión de actualización, y en este momento se comprueba de nuevo la expre sión de test. La sentencia for es también un bucle con condición de entrada; la decisión de atravesar el bucle una vez más se realiza antes de hacerlo. Es, por tanto, posible que el bucle no se atraviese ni una sola vez. La parte sen tencia del formato puede ser una sentencia simple o compuesta. Formato: for ( inicialización ; test ; actualización) sentencia;
El bucle se repite hasta que el test se vuelve falso o 0. Ejemplo: for (n = 0; n < 10; n++) printf(" %d %d\n", n, 2*n + 1);
La sentencia do while Palabras clave: do, while
Comentarios generales:
La sentencia do while crea un bucle que se repite hasta que la expresión le test se vuelve falsa o 0. La sentencia do while es un bucle con condición
Ejemplo: do scanf("%d", &numero); while (numero != 20);
Utilización de sentencias if para elegir entre opciones Palabras clave: if, else Comentarios generales:
En cada uno de los formatos siguientes, la sentencia puede ser simple o compuesta. Una expresión se considera cierta por generalización cuando su valor es distinto de 0. Formato 1: if ( expresión ) sentencia
La sentencia se ejecuta si la expresión es cierta. Formato 2: if ( expresión ) sentencia 1 else sentencia2 Si la expresión es cierta, se ejecuta la sentencia1. En caso contrario,
se ejecuta la sentencia2. Formato 3: if ( expresión1 ) sentencia1
else if ( expresión2 ) sentencia2 else sentencia3 Si la expresión1 es cierta, se ejecuta la sentencia1. Si la expresión1 es fal la, pero la expresión2 es cierta, se ejecuta la sentencia2. Si ambas expresiones son falsas, se ejecuta la sentencia3. Ejemplo: if (patas == 4) printf("Debe ser un caballo o.\n"); else if (patas > 4) printf("No es un caballo. \n") ; else /* se hace si patas < 4 */
Ejemplo: switch (letra)
{
case 'a' : case 'i' : print("%d es una vocal\n", letra); case 'c' : case 's' : printf("%d esta en la palabra \"casi\"\n", letra); default : printf("Que usted lo pase bien. \n"):
}
Si letra tiene el valor ‘a’ o ‘i’, se imprimen los tres mensajes; si vale 'c' o 's' se imprimen únicamente los dos últimos; cualquier otro valor imprimí simplemente el último mensaje.
Saltos en el programa
{ patas++; printf("Ahora tiene una pata mas.\n">; }
Palabras clave: break, continue, goto Comentarios generales:
Estas tres instrucciones producen un salto en el flujo de programa desde una localización a otra diferente.
Elección múltiple con switch break Palabra clave: switch Comentarios generales:
El control de programa salta a la sentencia etiquetada con el valor de la expresión. El flujo de programa continúa a través de la siguiente sentencia del switch, a menos que se vuelva a redirigir. Tanto la expresión como las etiquetas deben tener valores enteros (incluyendo tipo char), y las etiquetas deben ser constantes o expresiones formadas por constantes. Cuando ninguna de las etiquetas coincide con el valor de la expresión, el control se trans fiere a la sentencia etiquetada default, si existe. En caso contrario, se envía el control a la sentencia siguiente al switch. Formato:
switch ( expresión ) { case etiq1 : sentencia1 case etiq2 : sentencia2 default : sentencia3 } El número de sentencias etiquetadas puede ser mayor de dos, y el caso default es opcional.
El comando break se puede utilizar con cualquiera de los tres formato de bucle y, además, con la sentencia switch. Cuando el programa alcanza un break, se deja sin ejecutar el resto del bucle o switch que lo contiene, y se transfiere el control a la sentencia inmediatamente posterior a dicho bucle o switch. Ejemplo: switch (numero)
{
case 4: printf("Excelente eleccion!\n"); break; case 5: printf("Es una eleccion razonable.\n"); break; default: printf("Su eleccion es un asco.\n");
}
continue
El comando continue se puede emplear con cualquiera de los tres forma tos de bucle, pero no con switch. Al igual que el caso anterior, el programa no ejecuta las sentencias restantes del bucle donde se encuentra situado. En bucles for o while se comienza a ejecutar el siguente ciclo del bucle. En bu-
cles do while se comprueba la condición de salida; si se cumple, se comienza el nuevo ciclo. Ejemplo: while ( (ch = getchar() ) != EOF) if (ch == ' ' ) continue; putchar(ch); chcont ++;
}
Este fragmento produce un eco y cuenta caracteres que no sean espacios, goto
Una sentencia goto hace que el control de programa salte incondicional mente a la sentencia que contenga la etiqueta indicada. Se utiliza un símbolo dos puntos para separar la etiqueta de la sentencia etiqueta. Los nombres de etiquetas siguen las mismas reglas que los nombres de variables. La sentencia etiquetada puede estar situada antes o después del goto. Formato: goto etiq; . . . etiq : sentencia Ejemplo: tope : ch = getchar(); . . . if (ch != 's')
goto tope;
464