Sei sulla pagina 1di 652

INFORMTICA APLICADA PROGRAMACIN EN LENGUAJE C

Pedro Mara Alcover Garau Lenguajes y Sistemas Informticos

INFORMTICA APLICADA.

PROGRAMACIN EN LENGUAJE C.
Pedro Mara Alcover Garau

rea de Lenguajes y Sistemas Informticos Universidad Politcnica de Cartagena Septiembre, 2010 (Revisado Enero, 2011)

Pedro Mara Alcover Garau Edita Universidad Politcnica de Cartagena Enero 2011. ISBN: 978-84-693-9245-4 D.L. MU15332010. Imprime Morpi, S.L.

Nada de lo que dice este manual es necesario para la vida de nadie. Y ya que el asunto a tratar en estas pginas no me ha permitido escribir sobre tantas otras cosas que s me importan, verdaderamente importantes para la vida de todos al menos al redactar he querido hacerlo pensando en que cada uno de los alumnos de la UPCT s es importante para la vida de muchos. Y tambin son importantes para m. Como si fuera Dios quien anduviera en las aulas. Dios que ensea. Dios sentado en los pupitres.

NDICE.
NDICE. i a xii PRESENTACIN. 1

CAPTULO 1: INTRODUCCIN Y CONCEPTOS GENERALES.


Estructura funcional de las computadoras Instrucciones, lenguajes, compiladores Soporte fsico (hardware) y soporte lgico (software). Sistemas Operativos. Recapitulacin.

5
5 11 15 16

Informtica Aplicada. Programacin en Lenguaje C.

CAPTULO 2: CODIFICACIN NUMRICA.


Concepto de cdigo. Los nmeros como sistema de codificacin de cantidades. Fundamentos matemticos para un sistema de numeracin. Bases, dgitos y cifras. Sistemas de numeracin posicionales y bases ms habituales en el mundo de la informtica. Sistema Binario. Cambio de Base. Complementos a la Base. Recapitulacin. Ejercicios (2.1. a 2.3.).

17
17 19 20 22 23 25 27 30 31

CAPTULO 3: CODIFICACIN INTERNA DE LA INFORMACIN.


Introduccin. Cdigos de Entrada/Salida. Representacin o Codificacin Interna de la Informacin. Enteros sin signo. Enteros con signo. Recapitulacin. Ejercicios (3.1. a 3.3.).

37
37 39 41 42 43 44 44

ii

ndice.

CAPTULO 4: LENGUAJE C.
Introduccin. Entorno de programacin. Estructura bsica de un programa en C. Elementos lxicos. Sentencias simples y sentencias compuestas. Errores y depuracin. Estndar C89, Estndar C90 y Estndar C99 Recapitulacin.

49
49 51 54 56 58 59 60 61

CAPTULO 5: ALGORITMOS.
Concepto de Algoritmo. Representacin de algoritmos. Reglas bsicas de la programacin estructurada para la construccin de algoritmos. Otros ejemplos de construccin de algoritmos (5.1. a 5.5.). Recapitulacin. Otros ejercicios propuestos (5.6. a 5.12).

63
64 67 70 75 85 86

CAPTULO 6: MODELO DE REPRESENTACIN.


Introduccin Abstraccin

93
93 95

iii

Informtica Aplicada. Programacin en Lenguaje C.

Modularidad La abstraccin de la informacin: los datos. Tipo de dato. Variable. Variable - Tipo de dato Valor. Paradigmas de programacin. Programacin estructurada. Recapitulacin.

95 104 105 106 107 108 110

CAPTULO 7: TIPOS DE DATO Y VARIABLES EN C.


Declaracin de variables. Tipos de datos primitivos en C: sus dominios. Formacin de literales Tipos de datos primitivos en C: sus operadores. Operador asignacin. Operadores aritmticos. Una breve consideracin sobre el cociente de enteros Operadores relacionales y lgicos. Operadores a nivel de bit. Operadores compuestos. Operador sizeof. Expresiones en las que intervienen variables de diferente tipo. Operador para forzar cambio de tipo. Propiedades de los operadores.

113
114 116 119 121 122 124 127 128 131 137 138 139 141 143

iv

ndice.

Valores fuera de rango en una variable. Constantes (variables const). Directiva #define. Los enteros muy largos y otras consideraciones adicionales sobre tipos de dato en C90 y C99. Intercambio de valores de dos variables. Ayudas On line. Recapitulacin. Ejemplos y ejercicios propuestos (7.1. a 7.19.).

146 148 149 154 156 156 157

CAPTULO 8: FUNCIONES DE ENTRADA Y SALIDA POR CONSOLA.


Salida de datos. La funcin printf. Entrada de datos. La funcin scanf. Dificultades habituales con las entradas de caracteres Recapitulacin. Ejercicios (8.1 a 8.4.). Anexo I: Ficha resumen de la funcin printf.

175
176 186 188 192 193 201

CAPTULO 9: ESTRUCTURAS DE CONTROL I: ESTRUCTURAS DE SELECCIN O SENTENCIAS CONDICIONADAS.


Introduccin a las estructuras de control. Transferencia de control condicionada. Bifurcacin Abierta. La estructura condicional if.

205
206 207 209

Informtica Aplicada. Programacin en Lenguaje C.

Bifurcacin Cerrada. La estructura condicional if - else. Anidamiento de estructuras condicionales. Escala if else. La estructura condicional y el operador condicional. Estructura de seleccin mltiple: switch. Recapitulacin. Ejercicios (9.1. a 9.7.).

210 211 213 214 216 220 221

CAPTULO 10: ESTRUCTURAS DE CONTROL II: ESTRUCTURAS DE REPETICIN O SENTENCIAS ITERADAS.


Introduccin. Estructura while. Estructura do while. Estructura for. Sentencias de salto: break y continue. Palabra reservada goto. Variables de control de iteraciones. Recapitulacin. Ejercicios (10.21 a 10.29).

233
233 234 244 243 244 250 251 252 252

CAPTULO 11: ARRAYS NUMRICOS: VECTORES Y MATRICES.


Nocin y declaracin de array.

301
302

vi

ndice.

Nocin y declaracin de array de dimensin mltiple, o matrices. Arrays en el estndar C99 Ejercicios (11.1. a 11.27.).

304 308 309

CAPTULO 12: CARACTERES Y CADENAS DE CARACTERES.


Operaciones con caracteres. Entrada de caracteres. Cadena de caracteres. Dar valor a una cadena de caracteres. Operaciones con cadenas de caracteres. Otras funciones de cadena. Ejercicios (12.1. a 12.12.).

339
340 343 344 346 352 358 362

CAPTULO 13: MBITO Y VIDA DE LAS VARIABLES.


mbito y Vida. El almacenamiento de las variables y la memoria. Variables Locales y Variables Globales. Variables estticas y dinmicas. Variables en registro. Variables extern. En resumen Ejercicios (13.1.).

375
375 376 378 382 385 386 387 388

vii

Informtica Aplicada. Programacin en Lenguaje C.

CAPTULO 14: FUNCIONES.


Definiciones. Funciones en C. Declaracin de la funcin. Definicin de la funcin. Llamada a la funcin. La sentencia return. mbito y vida de las variables. Recapitulacin. Ejercicios (14.1. a 14.10.).

391
392 395 397 398 400 401 404 407 408

CAPTULO 15: RECURSIVIDAD (RECURRENCIA).


Resolviendo un ejercicio El concepto de recursividad. La demostracin por induccin o recurrencia. Definicin recurrente de conjunto. Demostracin recurrente de propiedades sobre conjuntos definidos recurrentemente. Etapas en la construccin de algoritmos recursivos. rbol de recursin. Recursin e iteracin. Ejercicio: las torres de Hanoi.

423
424 427 433 434 435 436 441 442 448

viii

ndice.

Recapitulacin. Ejercicios (15.1. a 15.6).

453 454

CAPTULO 16: PUNTEROS.


Definicin y declaracin. Dominio y operadores para los punteros. Punteros y vectores. ndices y operatoria de punteros. Puntero a puntero. Modificador de tipo const. Punteros constantes, punteros a constantes y punteros constantes a constantes Punteros fuera del mbito de la variable a la que apuntan. Ejercicios (16.1. y 16.2.).

463
464 465 469 473 475 480 481 484 487

CAPTULO 17: FUNCIONES Y PARMETROS CON PUNTEROS.


Llamadas por valor y llamadas por referencia. Vectores y matrices como argumentos. Argumentos tipo puntero constante. Recapitulacin. Ejercicios (17.1. a 17.11.).

491
492 485 498 499 500

ix

Informtica Aplicada. Programacin en Lenguaje C.

CAPTULO 18: ASIGNACIN DINMICA DE MEMORIA.


Funcin malloc. Funcin free. Ejemplo: la Criba de Erastthenes. Matrices en memoria dinmica. Ejercicios (18.1. y 18.2.).

521
523 527 528 529 533

CAPTULO 19: ALGUNOS USOS CON FUNCIONES.


Funciones de escape. Punteros a funciones. Vectores de punteros a funciones. Funciones como argumentos. Ejemplo: la funcin qsort. Estudio de tiempos. Creacin de MACROS. Ejemplo de MACRO: la Criba de Erastthenes. Funciones con un nmero variable de argumentos. Argumentos de la lnea de rdenes. Ejercicios (19.1. y 19.2.).

543
544 545 549 550 553 557 560 562 565 571 573

CAPTULO 20: ESTRUCTURAS ESTTICAS DE DATOS Y DEFINICIN DE TIPOS.

575

ndice.

Tipos de dato enumerados. Dar nombre a los tipos de dato. Estructuras de datos y tipos de dato estructurados. Estructuras de datos en C. Vectores y punteros a estructuras. Anidamiento de estructuras. Tipo de dato unin. Ejercicios (20.1.).

576 577 579 580 585 588 590 594

CAPTULO 21: GESTIN DE ARCHIVOS.


Tipos de dato con persistencia. Archivos y sus operaciones. Archivos de texto y binarios. Tratamiento de archivos en el lenguaje C. Archivos secuenciales con buffer. Entrada y salida sobre archivos de acceso aleatorio. Ejercicios (21.1. y 21.2.).

601
602 604 607 607 609 622 624

Cartagena, 15 de enero, 2011.

xi

Informtica Aplicada. Programacin en Lenguaje C.

xii

PRESENTACIN
Con sta, ya van tres las veces que he visto la necesidad de revisar el manual para los alumnos que redact hace cinco aos para aquellos que tenan que estudiar entonces la asignatura de Fundamentos de Informtica, en la Titulacin de Ingenieros Industriales. Con las nuevas titulaciones, llegadas de la mano del terremoto Bolonia, ha parecido conveniente rehacer de nuevo el manual. Se ha reestructurado la informacin. Han aumentado los captulos; tambin las pginas: lo siento de veras: no se me ocurra cmo mejorar el manual reduciendo la extensin de las explicaciones; y tampoco poda reducir el temario: creo que ms intil que no empezar es quedarse a mitad. Se incluyen muchos nuevos ejercicios resueltos, tomados de exmenes de aos anteriores. Hay una recomendacin clsica, que los profesores de programacin repetimos frecuentemente a los alumnos: en el aprendizaje de un lenguaje de programacin hay que programar: or permite olvidar; estudiar permite entender; slo programar permite aprender. No basta con ir a clase. No basta con estudiar este ladrillo. Hay que instalar un compilador y un editor de programas y ponerse delante de la pantalla. Y

Informtica Aplicada. Programacin en Lenguaje C.

programar. El aprendizaje de la programacin es una tarea inicialmente ingrata. Las horas de trabajo y estudio no parecen cundir. A una hora se sucede otra, en la que el aprendiz no entiende apenas sobre qu se le est instruyendo. Y otra hora, y otra, y otra Y uno se enfrenta al primer programa una autntica simpleza y no logra ms que una coleccin de 26 errores. Veintisis!: en tan slo cinco lneas de cdigo! Hay que seguir. Y detectar el punto y coma que falta en esa lnea; y el parntesis que en realidad deba ser una llave; y la biblioteca que no s bien para qu sirve y que para colmo el compilador no me la reconoce, porque la he deletreado mal; y mil pedradas ms que animan a cualquier cosa menos a seguir trabajando con el dichoso lenguaje C. Todos hemos tenido que aprender a programar. Para todos, esos primeros pasos han sido una personal pesadilla. Cada ao, en el primer con los nuevos alumnos, dibujo en la pizarra del aula una grfica que muestra el rendimiento del estudio de una materia como ste de un lenguaje de programacin. Es una curva a la que le cuesta levantarse del eje de las abscisas. Muestra, en su primera fase, una desesperante pendiente casi nula. Pero, en un entorno de valores

Rendimiento del estudio

Hay que alcanzar, al menos, ese punto de inflexin

Horas dedicada al estudio de un lenguaje

Presentacin.

determinado, distinto para cada estudiante, esa grfica sufre un cambio de pendiente. Casi de forma sbita: estudio y no comprendo estudio y no comprendo estudio y ah!, ahora lo veo!: un pequeo avance. Y con el paso de las horas, de repente se clarifican un bloque de conceptos, casi de golpe. Y el estudio cambia de cariz: el aprendiz se pica con la programacin. Ha llegado el momento de avanzar a buen ritmo. Se disfruta aprendiendo, porque se logran cosas nuevas, cada vez ms difciles. Se caza el modo en que se deben plantear las cosas al ordenador. Se coge la filosofa de cmo se construyen los programas. A uno se le ocurren nuevos retos, y se atreve a intentar buscar una solucin. Y se aprende entonces a programar. Y, por supuesto, se aprueba la asignatura. Lo que he contado en estas lneas previas es estadsticamente cierto. Porque a juzgar por las notas de los alumnos, o los exmenes son muy fciles, o los exmenes son muy difciles. Porque la media de los aprobados siempre est en el notable alto, con no pocos sobresalientes. Y la media de los no aprobados no alcanza al 2. Porque el que no llega a franquear el tiempo de estudio de inflexin, se queda en nada o en casi nada. Y el que lo supera, con poco esfuerzo alcanza un buen nivel. Que programar no es difcil; pero el inicio es la ms difcil de las fases de este aprendizaje. Agradecer recibir todas las sugerencias que ayuden a presentar de forma ms clara todo lo que se pretende explicar en este manual. As se podr ofrecer, a quienes vengan detrs, una versin del manual mejorada. Se puede contactar conmigo a travs del correo electrnico. Mi direccin es pedro.alcover@upct.es. Muchas gracias. Cartagena, 15 de enero de 2011

Informtica Aplicada. Programacin en Lenguaje C.

CAPTULO 1
INTRODUCCIN Y CONCEPTOS GENERALES.
El objetivo de este captulo primero es introducir algunas palabras de uso habitual entre quienes se ven en la necesidad de programar: lxico comn, de poca complejidad, pero que es necesario conocer bien para poder luego saber de qu se est hablando. Y presentar tambin muy sucintamente una descripcin de la arquitectura de un ordenador, que nos permitir comprender (de forma intuitiva: no se pretende ahora llegar ms all) cmo trabaja un ordenador con las instrucciones y datos que recibe al ejecutar un programa; y cmo logra un programador hacerse entender con una mquina que tiene un sistema de comunicacin y de interpretacin completamente distinto al humano.

Estructura funcional de las computadoras


Un esquema muy sencillo que representa la estructura bsica de una

Informtica Aplicada. Programacin en Lenguaje C.

computadora queda recogido en la figura 1.1.

Dispositivos de entrada

PROCESADOR

Dispositivos de salida

Figura 1.1.: Estructura bsica de una computadora El procesador recibe los datos desde los dispositivos de entrada (por ejemplo, teclado o ratn) y los muestra, o muestra los resultados del procesamiento, en los dispositivos de salida (por ejemplo pantalla o impresora). Los dispositivos de memoria masiva (disco duro, lpiz USB, disquete, CD/DVD) son dispositivos de entrada y de salida de informacin: el procesador puede leer la informacin grabada en ellos y puede almacenar en ellos nueva informacin. A estos dispositivos de memoria les llamamos de memoria masiva. La estructura bsica del procesador, de acuerdo con la que se llama arquitectura de Von Neumann (arquitectura en la que se basan la mayora de las actuales computadoras) queda esquematizada en la

CPU UC ALU

MEMORIA PRINCIPAL Datos

Controladores
Ctrol_1

Unidades E/S
unidad_1

...
Instrucciones
Ctrol_N

...
unidad_N

Buses del sistema


Figura 1.2.: Estructura bsica del procesador. Arquitectura de Von Neumann.

Captulo 1. Introduccin y conceptos generales.

figura 1.2. Von Neumann defini, de forma abstracta y conceptual, cuales deban ser las partes principales de la arquitectura de una computadora. Las soluciones finales que se han ido tomando para construir los ordenadores de acuerdo a ese diseo terico de la arquitectura Von Neumann, han ido cambiando a medida que la investigacin ha ofrecido mejores desarrollos tecnolgicos. Tambin se han producido ligeras modificaciones al primigenio modelo arquitectnico. La memoria principal es el lugar donde se almacenan los datos a procesar o ya procesados y los resultados obtenidos. Tambin se almacenan en ella el conjunto de instrucciones, o programas, que se desean ejecutar para el procesamiento de la informacin. Todo programa que se ejecute debe estar almacenado (cargado) en esta memoria principal. La memoria principal no forma parte de la CPU del ordenador, aunque hay autores que s la consideran parte de ella. Al margen de esa memoria principal, tanto la UC como la ALU disponen de registros propios de memoria y de bloques de memoria de acceso extremadamente rpido. La memoria principal est construida mediante circuitos de electrnica digital, muy sencillos que alcanzan dos estados estables posibles. Habitualmente se llaman a estos estados el estado cero y el estado uno. Es lo que se conoce como BIT (BInary digiT: dgito binario). El bit es la unidad bsica de informacin. No slo desde el punto de vista informtico. Con un bit es posible decir verdadero o falso; s o no. Los circuitos electrnicos que forman la memoria son circuitos

integrados, en los que se logra acumular una cantidad enorme de bits. Se podra hablar de la capacidad de memoria de una computadora, indicando el nmero de bits de que dispone. Pero habitualmente no se hace as, sino que se agrupan los bits para formar bloques de mayor capacidad de informacin. El agrupamiento ms conocido de bits es el llamado BYTE. Un byte es una agrupacin de 8 bits. Con bits juntos y

Informtica Aplicada. Programacin en Lenguaje C.

agrupados en una nica unidad de informacin ya se pueden codificar muchos ms que los dos valores posibles que alcanzbamos a codificar con un bit. En concreto, con un byte es posible obtener hasta combinaciones distintas: 00000000; 00000001; 00000010; 00000011; ; 11111101; 11111110; 11111111. Podemos decir, por tanto, que toda la memoria de la computadora est dividida en bloques (bytes). Estos bloques se disponen ordenadamente, de forma que se puede hacer referencia a uno u otro bloque concreto dentro del circuito integrado de memoria. Se puede hablar, por tanto, de posiciones dentro de la memoria; cada posicin est formada por un byte. As es posible crear un ndice de posiciones de memoria. Cada posicin tiene su ndice, al que llamamos direccin de memoria. La llamada memoria masiva es distinta cualitativamente de la memoria principal. Ya nos hemos referido a ella al presentarla como ejemplo de dispositivo de entrada y salida de informacin. Tambin se la conoce o llama memoria auxiliar, o secundaria. Esta memoria no goza de la misma velocidad que la principal, pero s logra ofrecer cantidades enormes de espacio donde almacenar datos de forma masiva. Ejemplos de esta memoria son el disco de una computadora, un DVD o un CD, o las cintas magnticas. La capacidad de almacenamiento de una computadora (o de cualquier soporte de informacin) se mide por el nmero de bytes de que dispone. De forma habitual se toman mltiplos de byte para esa cuantificacin. Entendemos por Kilobyte: bytes. Megabyte: los bytes. bytes, es decir 1.073.741.824 bytes, que es un valor del bytes. bytes, es decir 1.048.576 bytes, que es un del orden de bytes, es decir, 1.024 bytes, que es un valor cercano a

Gigabyte: orden de los

Captulo 1. Introduccin y conceptos generales.

Terabyte:

bytes, es decir 1.099.511.627.776 bytes, que es un valor bytes.

del orden de los Petabyte:

bytes, es decir 1.125.899.906.842.624 bytes, que es un bytes.

valor del orden de los

Otro componente que podemos ver en la Figura 1.2. es el denominado con el acrnimo UC: la Unidad de Control de la computadora. Es la encargada de interpretar cada instruccin del programa recibida desde la memoria, y de controlar su ejecucin una vez interpretada. Capta tambin las seales de estado que proceden de las distintas unidades de la computadora y que le informan (a la UC) de la situacin o condicin actual de funcionamiento (por ejemplo, informan de si un determinado perifrico est listo para ser usado, o de si un dato concreto est ya disponible). Y a partir de las instrucciones interpretadas y de las seales de estado recibidas, genera las seales de control que permitirn la ejecucin, instruccin tras instruccin, de todo el programa. Junto a la UC, vemos un segundo elemento que llamamos ALU (Unidad Aritmtico Lgica). Este bloque de nuestro procesador est formado por una serie de circuitos electrnicos capaces de realizar una serie de operaciones: as, con esa electrnica digital, se definen una coleccin de operadores aritmticos y operadores lgicos que producen resultados en su salida a partir de la informacin almacenada en sus entradas. La ALU, como los dems elementos de la computadora, trabaja a las rdenes de las seales de control generadas en la UC. Al conjunto de la UC y la ALU se le llama CPU (Unidad Central de Proceso). Ambos elementos han quedado agrupados por conveniencia de la tecnologa: no formaban una unidad en la arquitectura Von Neumann inicial. Existen algunos elementos ms que han quedado recogidos en los cuadros 1.1 y 1.2. Uno es el conjunto de los llamados controladores de entrada y salida, que permiten la correcta comunicacin entre el

Informtica Aplicada. Programacin en Lenguaje C.

procesador y los diferentes dispositivos de entrada y salida. El otro elemento es el formado por los buses del sistema, que comunican todas las unidades, y permiten el trasiego de datos (bus de datos), de direcciones de memoria (bus de direcciones) donde deben leerse o escribirse los datos, y de las diferentes sentencias de control generadas por la UC (bus de control). Para terminar esta rpida presentacin del procesador, conviene referir algunos elementos bsicos que componen la UC, la ALU, y la memoria principal. La Unidad de Control dispone, entre otros, de los siguientes registros de memoria de uso particular: Registro de instruccin, que contiene la instruccin que se est ejecutando. Contador de programa, que contiene permanentemente la direccin de memoria de la siguiente instruccin a ejecutar y la enva por el bus de direcciones. Decodificador, que se encarga de extraer el cdigo de operacin de la instruccin en curso. Secuenciador, que genera las micro-rdenes necesarias para ejecutar la instruccin decodificada. Reloj, que proporciona una sucesin de impulsos elctricos que permiten sincronizar las operaciones de la computadora. A su vez, la Unidad Aritmtico Lgica, dispone de los siguientes registros de memoria y elementos: Registros de Entrada, que contienen los operandos (valores que intervienen como extremos de una operacin) de la instruccin que se va a ejecutar. Registro acumulador, donde se almacenan los resultados de las operaciones. Registro de estado, que registra las condiciones de la operacin

10

Captulo 1. Introduccin y conceptos generales.

anterior. Circuito Operacional, que realiza las operaciones con los datos de los registros de entrada y del registro de estado, deja el resultado en el registro acumulador y registra, para prximas operaciones, en el registro de estado, las condiciones que ha dejado la operacin realizada. Y finalmente, la memoria dispone tambin de una serie de elementos, que se recogen a continuacin: Registro de direcciones, que contiene la direccin de la posicin de memoria a la que se va a acceder. Registro de intercambio, que recibe los datos en las operaciones de lectura y almacena los datos en las operaciones de escritura. Selector de memoria, que se activa cada vez que hay que leer o escribir conectando la celda de memoria a la que hay que acceder con el registro de intercambio. Seal de control, que indica si una operacin es de lectura o de escritura.

Instrucciones, Lenguajes, Compiladores.


Una instruccin es un conjunto de smbolos que representa (que codifica) una orden para el computador, que indica una operacin o tratamiento sobre datos. Y un programa es un conjunto ordenado de instrucciones que se le dan a la computadora y que realizan, todas ellas, un determinado proceso. Tanto las instrucciones, como los datos a manipular con esas

instrucciones, se almacenan en la memoria principal, en lugares distintos de esa memoria. Las instrucciones son solicitadas por la UC, y se envan desde la memoria hacia la Unidad de Control donde son interpretadas y donde se generan las seales de control que gobiernan los restantes elementos

11

Informtica Aplicada. Programacin en Lenguaje C.

del sistema. Los datos pueden intervenir como operandos en un procedimiento (informacin inicial) o ser el resultado de una secuencia determinada de instrucciones (informacin calculada). En el primer caso, esos datos van de la memoria a la Unidad Aritmtico Lgica, donde se realiza la operacin indicada por la presente instruccin en ejecucin. Si el dato es el resultado de un proceso, entonces el camino es el inverso, y los nuevos datos obtenidos son escritos en la memoria. El tipo de instrucciones que puede interpretar o ejecutar la UC depende, entre otros factores de la ALU de que dispone el procesador en el que se trabaja. De forma general esas instrucciones se pueden clasificar en los siguientes tipos: de transferencia de informacin (por ejemplo, copiar un valor de una posicin de la memoria a otra posicin); aritmticas, que bsicamente se reducen a la suma y la resta; lgicas, como operaciones relacionales (comparar valores de distintos datos) o funciones lgicas (AND, OR y XOR); de salto, con o sin comprobacin o verificacin de condicin de salto, etc. Las instrucciones a ejecutar deben ser codificadas de forma que la mquina las entienda. Ya hemos visto que todo en una computadora se codifica con bits, a base de ceros y unos. Con ceros y unos se debe construir toda informacin o sentencia inteligible para la computadora. Lograr comunicarse con la mquina en ese lenguaje de instrucciones que ella entiende (cdigo mquina) es una tarea difcil y compleja. A este galimatas hay que aadirle tambin el hecho de que los datos, evidentemente, tambin se codifican en binario, y que a stos los encontramos a veces en zonas de memoria distintas a la de las instrucciones, pero en otras ocasiones se ubican intercalados con las instrucciones. Un lenguaje as est sujeto a frecuentes errores de trascripcin. Y resulta completamente inexpresivo. Adems, otro problema, no pequeo, de trabajar en el lenguaje propio de una mquina es que un

12

Captulo 1. Introduccin y conceptos generales.

programa slo resulta vlido para esa mquina determinada, puesto que ante mquinas con distinta coleccin de microinstrucciones y diferente codificacin de stas, se tendr lgicamente lenguajes diferentes. El nico lenguaje que entiende directamente una mquina es su propio lenguaje mquina. Resulta mucho ms sencillo poder expresar las instrucciones que debe ejecutar la computadora en un lenguaje semejante al utilizado habitualmente por el hombre en su comunicacin. sa es la finalidad de los lenguajes de programacin. Pero un lenguaje as, desde luego, no lo entiende una mquina que slo sabe codificar con ceros y unos. Un lenguaje de programacin no puede tener la complejidad de un lenguaje natural de comunicacin entre personas. Un buen lenguaje de programacin debe permitir describir de forma sencilla los diferentes datos y estructuras de datos. Debe lograr expresar las distintas instrucciones que se le vayan a dar al computador para resolver un determinado problema de forma sencilla y precisa. Ha de resultar fcil escribir programas con l. As han surgido los distintos lenguajes de programacin, capaces de expresar instrucciones en unas sentencias que quedan a mitad de camino entre el lenguaje habitual y el cdigo mquina. Dependiendo del grado de semejanza con el lenguaje natural, o de la cercana con el lenguaje de la mquina, los lenguajes pueden clasificarse en distintas categoras: 1. El lenguaje de bajo nivel, o ensamblador, muy cercano y parecido al lenguaje mquina. Cada instruccin mquina se corresponde con una instruccin del lenguaje ensamblador, codificada, en lugar de con ceros y unos, con una agrupacin de tres o cuatro letras que representan, abreviadamente, la palabra (habitualmente inglesa) que realiza la operacin propia de esa instruccin. 2. Lenguajes de alto nivel, que disponen de instrucciones diferentes a

13

Informtica Aplicada. Programacin en Lenguaje C.

las que la mquina es capaz de interpretar. Habitualmente, de una instruccin del lenguaje de alto nivel se derivan varias del lenguaje mquina, y la ejecucin de una instruccin de alto nivel supone la ejecucin de muchas instrucciones en cdigo mquina. Normalmente esas instrucciones se pueden expresar de una forma cmoda y comprensible. Para resolver este problema de comunicacin entre la mquina y su lenguaje mquina y el programador y su lenguaje de programacin, se emplean programas que traducen del lenguaje de programacin al lenguaje propio de la mquina. Ese programa traductor va tomando las instrucciones escritas en el lenguaje de programacin y las va convirtiendo en instrucciones de cdigo mquina. Gracias a la capacidad de traducir un programa escrito en lenguaje de alto nivel al lenguaje mquina, se puede hablar de portabilidad en los lenguajes de alto nivel: la posibilidad de que un mismo programa pueda ser ejecutado en computadoras diferentes gracias a que cada una de ellas dispone de su correspondiente traductor desde el lenguaje en que va escrito el programa hacia el propio lenguaje mquina. Se disponen de dos diferentes tipos de traductores. Unos, llamados intrpretes, van traduciendo el programa a medida que ste se ejecuta. Cada vez que un usuario quiera ejecutar ese programa deber disponer del intrprete que vaya dictando a la computadora las instrucciones en cdigo mquina que logran ejecutar las sentencias escritas en el lenguaje de alto nivel. Un intrprete hace que un programa fuente escrito en un lenguaje vaya, sentencia a sentencia, traducindose y ejecutndose directamente por el ordenador. No se crea un archivo o programa en cdigo mquina. La ejecucin del programa debe hacerse siempre supervisada por el intrprete. Otro tipo de traductor se conoce como compilador: un compilador traduce todo el programa antes de ejecutarlo, y crea un programa en cdigo mquina, que puede ser ejecutado tantas veces como se quiera,

14

Captulo 1. Introduccin y conceptos generales.

sin necesidad de disponer del cdigo en el lenguaje de alto nivel y sin necesidad tampoco de tener el compilador, que una vez ha creado el nuevo programa en lenguaje mquina ya no resulta necesario para su ejecucin. Una vez traducido el programa al correspondiente lenguaje o cdigo mquina, su ejecucin es independiente del compilador.

Soporte fsico (hardware) y soporte lgico (software). Sistemas Operativos.


Se habla de hardware cuando nos referimos a cualquiera de los componentes fsicos de una computadora: la CPU, la memoria, un dispositivo de entrada Se habla de software para referirse a los diferentes programas que hacen posible el uso de la computadora para unas aplicaciones concretas. El hardware de una computadora se puede clasificar en funcin de su capacidad y potencia. Muy extendidos estn los llamados PC (computadoras profesionales o personales). Habitualmente trabajaremos en ellos y, de ahora en adelante, cuando queramos referirnos a una mquina o computadora pensaremos en un PC, al que llamaremos, sencillamente, ordenador. Un sistema operativo es un programa o software que acta de interfaz o conexin entre el usuario de un ordenador y el propio hardware del ordenador. Ofrece al usuario el entorno necesario para la ejecucin de los distintos programas. Un sistema operativo facilita el manejo del sistema informtico, logrando un uso eficiente del hardware del ordenador. Facilita al usuario la correcta ejecucin de los programas, las operaciones de entrada y salida de datos, la manipulacin de la informacin almacenada en la memoria, la deteccin de errores, Permite que el sistema tenga una racional y correcta asignacin de los recursos. Piense en su ordenador. El hecho de que al pulsar un carcter del

15

Informtica Aplicada. Programacin en Lenguaje C.

teclado aparezca una representacin de ese carcter en la pantalla no es cosa trivial. Ni el que al mover su ratn se desplace de forma proporcionada una flecha o cursor en esa pantalla. Quin se encarga de que lo que usted ha creado con un programa se guarde correctamente en el disco duro: por ejemplo, un escrito creado con un editor de texto? Quin gestiona los archivos en carpetas, y los busca cuando usted no recuerda dnde estaban? Cmo logra el ordenador lanzar varios documentos a la impresora y que stos salgan uno tras otro, de forma ordenada, sin colapsar el servicio ni sobrescribirse? Por qu al hacer doble click en un icono se ejecuta un programa? Cmo elimino de mi ordenador un archivo que ya no necesito?: no crea que basta con arrastrar ese archivo a lo que todo el mundo llamamos la papelera: nada de todo eso es trivial. Todos requerimos de nuestro ordenador muchas operaciones, que alguien ha tenido que disear y dejar especificadas. sa es la tarea del sistema operativo. Sistemas operativos conocidos son, por ejemplo, el Unix, o su versin para PC llamada Linux, y el comercialmente extendido Windows, de Microsoft.

Recapitulacin.
Hemos introducido algunas nociones necesarias para poder luego enfrentarnos al estudio de la programacin y, en concreto, de un lenguaje como el lenguaje C: qu es un lenguaje de programacin y cmo se logra que un ordenador sea capaz de interpretar correctamente las instrucciones que el programador le indica con un determinado lenguaje. Y hemos presentado los conceptos bsicos necesarios para conocer cmo est construido y cmo funciona internamente un ordenador, y cmo es posible que realice una serie de operaciones perfectamente definidas y en correcta secuencia.

16

CAPTULO 2
CODIFICACIN NUMRICA.
El objetivo de este captulo es mostrar el concepto de cdigo, y especficamente y ms en concreto presentar unas nociones bsicas sobre la forma en que se codifican las cantidades mediante nmeros.

Concepto de cdigo.
Si se busca en el diccionario de la Real Academia Espaola el significado de la palabra cdigo, se encuentra, entre otras acepciones, las siguientes: Combinacin de signos que tiene un determinado valor dentro de un sistema establecido. El cdigo de una tarjeta de crdito. Sistema de signos y de reglas que permite formular y comprender un mensaje. Podramos decir que un cdigo es una relacin ms o menos arbitraria

Informtica Aplicada. Programacin en Lenguaje C.

que se define entre un conjunto de mensajes o significados a codificar y un sistema de signos que significan esos mensajes de forma inequvoca. El cdigo no es tan solo el conjunto de signos, sino tambin la relacin que asigna a cada uno de esos signos un significado concreto. Ejemplos de cdigos hay muchos: desde el semforo que codifica tres posibles mensajes con sus tres valores de cdigo diferentes (rojo, mbar y verde) hasta el sistema de signos que, para comunicarse, emplean las personas sordas. O el cdigo de banderas, o el sistema Braille para los invidentes que quieren leer. Para establecer un cdigo es necesario cuidar que se verifiquen las siguientes propiedades: 1. Que quede bien definido el conjunto de significados o mensajes que se quieren codificar. En el ejemplo del semforo, queda claro que hay tres mensajes ntidos: adelante / alto / precaucin. No hay confusin, ni posibilidad de equvoco en estos tres mensajes. 2. Que quede bien definido el conjunto de los elementos que van a codificar o significar esos mensajes. En el caso del semforo queda claro cul es el cdigo de colores y no hay espacio para la confusin. Excepto para quien tenga algn tipo de limitacin con la vista. 3. Que no haya ms significados que signos. Si hay ms signos que mensajes a codificar, el cdigo es vlido, pero tendremos mensajes codificados de distintas maneras (redundancias) o tendremos signos que no signifiquen nada. Si hay ms mensajes que signos, el cdigo no es vlido porque tendremos mensajes que no estn codificados, o tendremos signos que signifiquen varias cosas diferentes, lo que ser causa de equvocos. Lo mejor es siempre que un cdigo se formule mediante una aplicacin biyectiva, que establezca una relacin entre significados y signos, que asigne a cada significado un signo y slo uno, y que todo signo signifique un significado y slo uno.

18

Captulo 2. Codificacin numrica.

Los nmeros como sistema de codificacin de cantidades.


Para significar cantidades se han ideado muchos sistemas de representacin, o cdigos. Todos conocemos el sistema romano, que codifica cantidades mediante letras. Ese sistema logra asignar a cada cantidad una nica combinacin de letras. Pero, por desgracia, no es un sistema que ayude en las tareas algebraicas. Quin es capaz de resolver con agilidad la suma siguiente: CMXLVI + DCCLXIX? El sistema de numeracin arbigo, o indio, es en el que nosotros estamos habituados a trabajar. Gracias a l codificamos cantidades. Decir CMXLVI es lo mismo que decir 946; o decir DCCLXIX es lo mismo que decir 769. Son los mismos significados o cantidades codificados segn dos cdigos diferentes. Un sistema de numeracin es un conjunto de smbolos y reglas de generacin que permiten construir todos los nmeros vlidos en el sistema. Un sistema de numeracin es un cdigo que permite codificar cantidades mediante nmeros. Las cantidades se codifican de una manera u otra en funcin del sistema de numeracin elegido. Un nmero codifica una cantidad u otra en funcin del sistema de numeracin que se haya seleccionado. Un nmero es un elemento de un cdigo. Qu codifica un nmero?: Un nmero codifica una cantidad.

negros.

En la lnea inmediatamente anterior hemos dibujado una serie de puntos

Cuntos son esos puntos es una cuestin que en nada depende del

19

Informtica Aplicada. Programacin en Lenguaje C.

sistema de numeracin. La cantidad es la que es. Al margen de cdigos. En nuestro sistema habitual de codificacin numrica (base 10) diremos que tenemos 7 puntos. Pero si trabajamos en el sistema binario de numeracin, diremos que tenemos 111 puntos. Lo importante es que tanto la codificacin 7 (en base diez) como la codificacin 111 (en base dos) significan la misma cantidad. Y es que trabajar en base 10 no es la nica manera de codificar cantidades. Ni tampoco es necesariamente la mejor.

Fundamentos matemticos para un sistema de numeracin. Bases, dgitos y cifras.


Todo nmero viene expresado en una base . Una base es un conjunto finito y ordenado de signos. . Sus propiedades pueden resumirse en las tres siguientes: El primer elemento de la base es el cero (este dgito es

imprescindible en los sistemas de numeracin posicionales: concepto que veremos ms adelante en este captulo). Los sucesivos elementos ordenados de la base son tales que cualquier elemento es igual a su inmediato anterior ms uno. El mximo valor de la base es igual al cardinal de la base menos uno. La base , por ejemplo, est formada por los siguientes elementos: . En cada base, todo nmero entero nico en la forma . (2.1.) puede ser escrito de modo

20

Captulo 2. Codificacin numrica.

donde

es un entero, y cada uno de los , para ,y

son enteros que verifican (2.2.) . A la expresin

A los coeficientes ai se les llama dgitos del nmero representa como .

(2.1.) se la llama expansin del nmero. El nmero habitualmente se

(2.3.)

Cualquier nmero viene representado, en una base determinada, por una serie nica de coeficientes base, se le llama cifra. En una cifra importa tanto la posicin relativa de cada dgito dentro de ella, como el valor de cada uno de esos dgitos. A este tipo de sistemas de numeracin, en los que importa la posicin del dgito dentro de la cifra, se llaman sistemas de numeracin posicionales. No es lo mismo el nmero 567 que el 675, aunque ambos empleen la misma cantidad de y los mismos dgitos. Cuanto ms larga pueda ser la serie de dgitos que se emplean para codificar, mayor ser el rango de nmeros que podrn ser representados. Por ejemplo, en base , si disponemos de tres (ver 2.3.). A cada serie de coeficientes, que codifica un nmero de modo nico en una determinada

dgitos podremos codificar 1000 valores diferentes (desde el 000 hasta el 999); si disponemos de cinco dgitos podremos codificar 100.000 valores (desde el 00000 hasta el 99999). Como se sabe, y como se desprende de esta forma de codificacin, todo cero a la izquierda de estos dgitos supone un nuevo dgito que no aporta valor alguno a la cantidad codificada. La expansin del nmero recoge el valor de cada dgito y su peso dentro de la cifra. El dgito comprendido entre 0 y del nmero puede tomar cualquier valor . Cuando se necesita codificar un nmero

21

Informtica Aplicada. Programacin en Lenguaje C.

mayor o igual que el cardinal de la base ( ) se requiere un segundo dgito el valor dgito , que tambin puede tomar sucesivamente todos los valores . Cada vez que el dgito debiera superar se hace , vuelve a tomar el valor inicial 0 y se incrementa en uno el . Cuando el dgito necesita superar el valor en la cifra, que tambin podr debiera superar el alcanza contabiliza el supone supone . incrementos comprendidos entre 0 y

necesario introducir un tercer dgito

tomar sucesivamente todos los valores comprendidos entre 0 y incrementndose en uno cada vez que el dgito valor . El dgito en sus incrementos el valor superior a nmero de veces que incrementos del dgito incrementos del dgito en . contabiliza el nmero de veces que . El dgito

alcanza en sus incrementos el valor superior a . El incremento en uno del dgito , lo que a su vez supone exige

. Por tanto, el incremento en uno del dgito

incrementos de

Y as, sucesivamente, el incremento del dgito

Todos los dgitos posteriores a la posicin que el dgito comprendidos entre 0 y .

codifican el nmero de veces

ha recorrido de forma completa todos los valores

De todo lo dicho ahora se deduce que toda base es, en su sistema de numeracin, base 10: Dos, en base binaria se codifica como 10; tres, en base 3, se codifica como 10; cuatro, en base 4, se codifica como 10

Sistemas de numeracin posicionales y bases ms habituales en el mundo de la informtica.


El sistema de numeracin ms habitual en nuestro mundo es el sistema decimal. Si buscamos un porqu a nuestra base 10 quiz deduzcamos que su motivo descansa en el nmero de dedos de nuestras dos manos. Pero un ordenador no tiene manos. Ni dedos. Como hemos visto en el captulo anterior, el circuito electrnico bsico

22

Captulo 2. Codificacin numrica.

de la memoria de los ordenadores, tal como hoy se conciben, est formado por una gran cantidad de circuitos electrnicos que tiene dos estados estables posibles. Cuntos estados posibles?...: DOS. Por eso, porque los ordenadores slo tienen dos dedos, es por lo que ellos trabajan mejor en base dos. Es decir, slo disponen de dos elementos para codificar cantidades. El primer elemento, por definicin de base, es el valor cero. El segundo (y ltimo) es igual al cardinal de la base menos uno y es igual al primer elemento ms uno. Esa base est formada, por tanto, por dos elementos que llamaremos: Otras bases muy utilizadas en programacin son la base octal (o base ocho) y la base hexadecimal (o base diecisis). Lo de la base hexadecimal puede llevar a una inicial confusin porque no nos imaginamos qu dgitos podemos emplear ms all del dgito nueve. Para esa base se extiende el conjunto de dgitos haciendo uso del abecedario: .

Sistema Binario.
Aprender a trabajar en una base nueva no est exento de cierta dificultad. Habra que echar mano de nuestra memoria, de cuando ramos infantes y no sabamos contar. No nos resultaba sencillo saber qu nmero viene (en base diez) despus del noventa y nueve. Noventa y ocho, Noventa y nueve, Noventa y diez. No!: cien. Trabajemos en base diez:
0 10 20 1 11 21 2 12 22 3 13 23 4 14 24 5 15 25 6 16 26 7 17 27 8 18 28 9 19 29

23

Informtica Aplicada. Programacin en Lenguaje C.

y en base dos?: despus de cero el uno. Y despus del uno el diez!


0 1010 10100 1 1011 10101 10 1100 10110 11 1101 10111 100 1110 11000 101 1111 11001 110 10000 11010 111 10001 11011 1000 10010 11100 1001 10011 11101

En ambos cuadros estn codificadas las mismas cantidades. En base diez el primero, en base dos o base binaria el segundo. Adems de contar, es necesario aprender las operaciones matemticas bsicas. Al menos sumar y restar. De nuevo hay que volver a la infancia y aprender la aritmtica bsica de los clsicos cuadernos de sumas. Se acuerda de los famosos cuadernos Rubio: http://www.rubio.net/? Las reglas bsicas para esas dos operaciones (suma y resta) son: 0 + 0 = 0 0 + 1 = 1 1 + 0 = 1 1 + 1 = 0 y llevo 1 0 0 = 0 0 1 = 1 y debo 1 1 0 = 1 1 1 = 0

Y as, se puede practicar con sumas de enteros de ms o menos dgitos:

10100 +1110 100010

11101 +10111 110100

1011011011 +1100011 1100111110

1010000110 +1100001110 10110010100

100001001 +11101001 111110010

Para las restas haremos lo mismo que cuando restamos en base diez: el minuendo siempre mayor que el sustrayendo: en caso contrario se invierten los valores y al resultado le cambiamos el signo:

10100 -1110 00110

11101 -10111 00110

1011011011 -1100011 1001111000

1100001110 -1010000110 0010001000

100001001 -11101001 000100000

24

Captulo 2. Codificacin numrica.

El mejor modo de aprender es tomar papel y bolgrafo y plantearse ejercicios hasta adquirir soltura y destreza suficiente para sentirse seguro en el manejo de estos nmeros del en un sistema de numeracin binario. Al final del captulo se recoge una sugerencia til para practicar las operaciones aritmticas: realizarlas en la calculadora de Windows y probar luego a realizar esas mismas operaciones a mano.

Cambio de Base.
Las cantidades que se codifican no dependen del sistema de codificacin. Antes hemos visto los treinta primeros nmeros naturales (comenzando por el cero) codificados en base diez y en base dos. Cmo obtener, a partir de una cantidad codificada en base dos, su codificacin en base diez? Y viceversa? Paso de base dos a base diez: Para este cambio de base es suficiente con desarrollar la expansin del nmero. Por ejemplo:

. Paso de base diez a base dos: Para este cambio se divide el entero por dos (divisin entera), y se repite sucesivamente esta divisin hasta llegar a un cociente menor que la base. Simplemente vamos dividiendo por la base el nmero original y vamos repitiendo el procedimiento para los cocientes que vamos obteniendo. Los restos de estas divisiones, y el ltimo cociente, son los dgitos buscados (siempre debern estar entre 0 y ). El ltimo cociente es el dgito ms significativo, y el primer resto el menos significativo. Por ejemplo, en el ejemplo recogido en el Cuadro 2.1. vemos que el valor 157 expresado en base diez es, en base dos, 10011101. Las bases octal y hexadecimal, a las que antes hemos hecho referencia,

25

Informtica Aplicada. Programacin en Lenguaje C.

facilita el manejo de las cifras codificadas en base dos, que enseguida acumulan gran cantidad de dgitos, todos ellos ceros o unos. Para pasar de base dos a base diecisis es suficiente con separar la cifra binaria en bloques de cuatro en cuatro dgitos, comenzando por el dgito menos significativo. Al ltimo bloque, si no tiene cuatro dgitos, se le aaden tantos ceros a la izquierda como sean necesarios.

157 1

2 78 0 2 39 1 2 19 1 2 9 1 2 4 0 2 2 0 2 1

Cuadro 2.1.: De base diez a base dos. La equivalencia entre la base dos y la base diecisis es inmediata sabiendo que diecisis es dos a la cuarta. En la tabla 2.1. se recogen todas las equivalencias. BINARIO 0000 0001 0010 0011 0100 0101 0110 0111 DEC 0 1 2 3 4 5 6 7 HEX 0 1 2 3 4 5 6 7 BINARIO 1000 1001 1010 1011 1100 1101 1110 1111 DEC 8 9 10 11 12 13 14 15 HEX 8 9 A B C D E F

Tabla 2.1.: Equivalencias binario hexadecimal Por ejemplo, la cantidad 10011101 expresada en base dos, queda, en

26

Captulo 2. Codificacin numrica.

base diez, 157 y, en base hexadecimal, 9D: los cuatro ltimos dgitos binarios son 1101 que equivale al dgito D hexadecimal. Y los otros cuatro dgitos binarios son 1001, que equivalen al 9 hexadecimal. No es necesario, para cambiar de base decimal a base hexadecimal, hacer el paso intermedio por la base binaria. Para pasar de cualquier base a la base decimal basta con la expansin del nmero: ver expresin 2.1. Por ejemplo, el nmero 4E8, expresado en base hexadecimal, sera, en base diez, el siguiente: . donde, como se ve, se cambian los valores de los dgitos mayores que nueve por su equivalente decimal. El cambio a la base octal es muy semejante a todo lo que se ha visto para el cambio a la base hexadecimal. De la Tabla 2.1. basta tener en consideracin la columna de la izquierda. Los dgitos de la base octal son los mismos que para la base decimal, excluidos el 8 y el 9.

Complementos a la Base.
Vamos a introducir dos conceptos nuevos, muy sencillos: los de complemento a la base y complemento a la base menos uno. Supongamos el nmero expresado en una base determinada. Y

supongamos que ese nmero tiene

dgitos. Llamamos Complemento dgitos, en el que el

a la base de ese nmero expresado en esa base determinada, a la cantidad que le falta para llegar a la cifra de cero. De una forma ms precisa, definiremos el complemento a la base de un nmero codificado con . cifras en base como (2.4.) dgito ms significativo es el uno y los dems son todos ellos iguales a

27

Informtica Aplicada. Programacin en Lenguaje C.

Por ejemplo, en base diez, el complemento a la base del nmero es la cantidad que hace falta para llegar a , que es .

En esta definicin hay que destacar que, si el nmero

viene expresado

de forma que a su izquierda se tienen algunos dgitos iguales a cero, entonces el complemento a la base es diferente que si estuviese sin esos dgitos, aunque la cantidad codificada sera la misma. Siguiendo con el ejemplo anterior, el complemento a la base del nmero codificado como ya no es , sino , porque ahora no se trata de , sino para llegar a , calcular lo que falta para llegar a dgitos. El concepto de Complemento a la Base menos uno es muy semejante: es la cantidad que dista entre el nmero dgitos en la base , y el nmero formado por el valor del mayor elemento de la base , codificado con dgitos, todos ellos con

puesto que ahora la cantidad numrica est codificado con cuatro

en la que se trabaja.

De una forma ms precisa, definiremos el complemento a la base menos uno de un nmero codificado con . Por ejemplo el complemento a la base menos uno del nmero expresado en base diez es la cantidad que hace falta para llegar a , que es . cifras en base como (2.5.)

De nuevo aqu tambin cambia el complemento a la base menos uno de un nmero si ponemos en su codificacin ms o menos ceros a su izquierda. Es inmediato tambin deducir que la relacin entre los dos

complementos es que la diferencia entre ambos es igual a uno. De hecho se puede definir el Complemento a la Base menos uno de un nmero codificado con dgitos en la base , como el Complemento a , menos 1. la Base de un nmero codificado con dgitos en la base

28

Captulo 2. Codificacin numrica.

(2.6.)

Una curiosidad de los complementos es que, en cierta medida, facilitan el clculo de las restas. Se pueden ver algunos ejemplos en base diez. Se cumple que la resta de dos enteros se puede tambin calcular haciendo la suma entre el minuendo y el complemento a la base del sustrayendo, despreciando el posible acarreo final. Por ejemplo:

619 492 127

El complemento a la base de 492 es 508

619 + 508 1127

Donde si, como se ha dicho, despreciamos el ltimo acarreo, tenemos que se llega al mismo resultado: 127. Tambin se puede realizar un procedimiento similar si se trabaja con el complemento menos uno. En ese caso, lo que se hace con el ltimo acarreo, si se tiene, es eliminarlo del resultado intermedio y sumarlo para llegar al resultado final. Por ejemplo:

619 492 127

El complemento a la base menos uno de 492 es 507 Sumamos el acarreo a la cantidad obtenida sin acarreo

619 + 507 1126 +1 127

Hasta ahora todo lo expuesto puede aparecer como un enredo algo intil porque, de hecho, para el clculo del complemento a la base es necesario realizar ya una resta, y no parece que sea mejor hacer la resta mediante uno de los complementos que hacerla directamente. Pero las cosas cambian cuando se trabaja en base dos. Veamos algunos ejemplos de clculo de los complementos en esa base:

29

Informtica Aplicada. Programacin en Lenguaje C.

C2 (N)
01010 00110000111 011

C1(N)
01001 00110000110 010

10110 11001111001 101

Si se compara la codificacin de cada nmero estn cambiados: all donde en tiene un 0; y viceversa.

con su correspondiente se

complemento a la base menos uno, se descubre que todos los dgitos corresponde el dgito 1, en

Es decir, que calcular el complemento a la base menos uno de cualquier nmero codificado en base dos es tan sencillo como cambiar el valor de cada uno de los dgitos de la cifra. Y esta operacin es muy sencilla de realizar con circuitos electrnicos. La ventaja clara que aportan los complementos es que no es necesario incorporar un restador en la ALU, puesto que con el sumador y un inversor se pueden realizar restas.

Recapitulacin.
En este captulo hemos visto que las cantidades pueden ser expresadas de muy variadas formas, de acuerdo con el sistema de codificacin numrico que se quiera emplear. Y hemos estudiado las equivalencias entre los nmeros codificados en diferentes bases. Es conveniente que los conceptos introducidos en este captulo estn bien trabajados: no tienen especial complicacin, pero requiere un mnimo de esfuerzo y trabajo afianzar la nocin de base. Es conveniente practicar diferentes cambios de base, y acostumbrarse a operar en base dos y en base hexadecimal. Tambin es conveniente practicar en la bsqueda de complementos: de hecho, como ya veremos en el prximo captulo, el ordenador echa mano de los complementos a la base para almacenar la informacin.

30

Captulo 2. Codificacin numrica.

Vale la pena que se realicen diferentes clculos y se verifique que realmente se ha llegado a resultados correctos.

Ejercicios.

2.1.

Cambio de base: Expresar

en hexadecimal.

Podemos emplear dos caminos: o pasarlo a base 2 (por divisiones sucesivas por esa base) y posteriormente pasarlo a base hexadecimal; o pasar el valor directamente a base hexadecimal (dividiendo sucesivamente por 16). Tomamos esa ltima va: 810 10 16 50 2 Y el resultado es 16 3 : el dgito ms significativo, el ltimo

cociente (aquel que ya es menor que el divisor, que es la base); y luego, uno detrs de otro, todos los restos, desde el ltimo hasta el primer resto obtenido. En la base hexadecimal existe un elemento de la base que codifica la cantidad 10: la letra A. As lo podemos ver en la tabla 2.1. Podemos expresar ese nmero en cualquier otra base: por ejemplo, en base octal pasamos a base 5 . Por ltimo, como tercer ejemplo, lo . Las divisiones realizadas para

estas dos conversiones son las siguientes: 810 2 8 101 5 8 12 4 8 1 810 0 5 162 2 5 32 2 5 6 1 5 1

31

Informtica Aplicada. Programacin en Lenguaje C.

Que se puede verificar prontamente calculando la expansin (expresin 2.1.) del nmero en base 5, para pasarlo a la base decimal:

Realizar sumas, restas, y productos en base binaria: 1 * 1 1 1 1 0 1 1 1 1 1 1 1 1 0 1 1 0 1 1 0 1 0 0 0 0 0 0 0 1 0 0 1 0 1 1 1 1 1 1 1 0 1 1 1

+ 1

1 0

1 0

1 0

1 0

1 0 1

1 1 0 1

Que en base 10 resulta ser 243 * 71 = 17253. 10010010010101100 + 10000111110011011 = 100011010001000111 que en base 10 resulta ser 74924 + 69531 = 144455. 10010010011111110 10000110111101100 = 1011100010010 que en base 10 resulta ser 75006 69100 = 5906.

2.2.

Verificar la resta anterior mediante la suma del minuendo y el complemento a la base menos 1 del sustrayendo.

Primero

calculamos

el

complemento

la

base

menos .

uno

de

sustrayendo:

Luego calculamos el complemento a la base sin ms que sumar 1 al valor previo obtenido: Y ahora hacemos la suma del minuendo ms este ltimo valor 10010010011111110 +01111001000010100 100001011100010010 Y despreciando el ltimo acarreo, nos queda: 1011100010010, que el .

32

Captulo 2. Codificacin numrica.

mismo que habamos obtenido con la diferencia directa.

2.3.

Calcular los complementos a la base de los siguientes nmeros (expresados en base 10).

Sugerencia
Un modo cmodo y rpido de lograr muchos ejemplos de cambio de base y de codificacin de enteros es utilizar la calculadora de Windows. Si en el men Ver se selecciona la calculadora Cientfica, se obtiene una como la presentada en la figura 2.1. Debajo del visor, a su izquierda, se

Figura 2.1.: Calculadora de Windows

33

Informtica Aplicada. Programacin en Lenguaje C.

puede seleccionar la base en que se quieren introducir los nmeros. Si se introduce un valor en base decimal (Opcin Dec), luego se pueden ver esa misma cantidad codificada en cualquiera de las otras tres bases que ofrece esa calculadora: hexadecimal (opcin Hex), octal (opcin Oct) binaria (opcin Bin). Dependiendo de la base seleccionada, quedan activados o desactivados unos u otros de los botones de introduccin de dgitos en la calculadora. Como se ve en la figura 2.2., cuando se trabaja en base hexadecimal, octal o binaria, en la parte derecha y debajo del visor, aparece un selector de cuatro posibilidades. Si se selecciona la ms a la derecha, la que pone Byte, entonces la calculadora trabajar con enteros de 8 bits. Si elegimos Word, entonces trabajar con 16 bits (2 bytes). Con la siguiente opcin, DWord llegamos a 32 bits. Y la ltima opcin, QWord podremos trabajar incluso con 64 bits (enteros de 8 bytes). Con eso, podemos ver, por ejemplo, que el nmero (8451)10 se expresa, en base binaria, como (10000100000011)2 . Para trabajar con mayor comodidad cuando se ven los nmeros en base dos, se puede seleccionar, en el men Ver de la calculadora, la opcin Nmero de dgitos en grupo. As, todas las cantidades expresadas en

Figura 2.2.: Expresin del valor hexadecimal 2103 o del valor decimal 8451, aqu en base binaria.

34

Captulo 2. Codificacin numrica.

binario vendrn agrupadas en dgitos de cuatro en cuatro, mostrando con facilidad la equivalencia entre esa expresin binaria y la misma utilizando dgitos hexadecimales.

35

Informtica Aplicada. Programacin en Lenguaje C.

36

CAPTULO 3
CODIFICACIN INTERNA DE LA INFORMACIN.
El objetivo de este captulo es mostrar algunas formas habituales en que un ordenador codifica la informacin. Es conveniente conocer esta codificacin: cmo fluye la informacin de los perifricos hacia el procesador o al revs; y cmo codifica el procesador la informacin en sus registros, o en la memoria principal, o en los circuitos de la ALU. Y resulta adems de utilidad en las tareas del programador, que puede obtener muchas ventajas en el proceso de la informacin si conoce el modo en que esa informacin se encuentra disponible en las entraas del ordenador.

Introduccin.
La informacin, en un ordenador, se almacena mediante datos codificados con ceros y unos. Ya lo hemos visto en los captulos

Informtica Aplicada. Programacin en Lenguaje C.

precedentes. Ahora, en este captulo, queremos ver cmo son esos cdigos de ceros y unos. No nos vamos a entretener en la codificacin de todos los posibles tipos de dato. Hemos centrado principalmente la presentacin de este captulo en el modo cmo se codifican los enteros. Y eso por dos motivos: porque es un cdigo muy sencillo; y porque resulta de gran utilidad conocer esa codificacin: como veremos, el lenguaje C ofrece herramientas para poder manipular ese cdigo y obtener, si se sabe, resultados interesantes y ventajosos. Al tratar de los datos a codificar, deberemos distinguir entre la codificacin que se emplea para la entrada y salida de datos, y la que el ordenador usa para su almacenamiento en memoria. Por ejemplo, si el usuario desea introducir el valor 412, deber pulsar primero el cuatro, posteriormente la tecla del uno, y finalmente la del dos. El modo en que el teclado codifica e informa a la CPU de la introduccin de cada uno de estos tres caracteres ser diferente al modo en que finalmente el ordenador guardar en memoria el valor numrico 412. As las cosas, el modo en que un usuario puede suministrar informacin por teclado a la mquina es mediante caracteres, uno detrs de otro. Estos caracteres podemos clasificarlos en distintos grupos: 1. De Texto: a. Alfanumricos: i. Alfabticos: de la a a la z y de la A a la Z. ii. Numricos: del 0 al 9. b. Especiales: por ejemplo, (, ). +. ?. @, 2. De control: por ejemplo, fin de lnea, tabulador, avance de pgina, etc. 3. Grficos: por ejemplo, , , , Como el ordenador slo dispone, para codificar la informacin, de ceros y de unos, deberemos establecer una correspondencia definida entre el

38

Captulo 3. Codificacin interna de la informacin.

conjunto de todos los caracteres y un conjunto formado por todas las posibles secuencias de ceros y de unos de una determinada longitud. A esa correspondencia la llamamos cdigo de Entrada/Salida. Existen muchos distintos cdigos de E/S, algunos de ellos normalizados y reconocidos en la comunidad internacional. Desde luego, cualquiera de estas codificaciones de E/S son arbitrarias, asignando a cada carcter codificado, una secuencia de bits, sin ninguna lgica intrnseca, aunque con lgica en su conjunto, como veremos. Estos cdigos requieren de la existencia de tablas de equivalencia uno a uno, entre el carcter codificado y el cdigo asignado para ese carcter. Y, como acabamos de decir, esta codificacin es distinta de la que, una vez introducido el dato, emplear el ordenador para codificar y almacenar en su memoria el valor introducido. Especialmente, si ese valor es un valor numrico. En ese caso especialmente, tiene poco sentido almacenar la informacin como una cadena de caracteres, todos ellos numricos, y resulta mucho ms conveniente, de cara tambin a posibles operaciones aritmticas, almacenar ese valor con una codificacin numrica binaria. En ese caso, estamos hablando de la representacin o codificacin interna de los nmeros, donde ya no se sigue un criterio arbitrario o aleatorio, sino que se toman en consideracin reglas basadas en los sistemas de numeracin posicional en base dos.

Cdigos de Entrada/Salida.
Ya hemos quedado que esta codificacin es arbitraria, asignando a cada carcter del teclado un valor numrico que queda codificado en las entraas del ordenador mediante una cifra en base binaria de una longitud determinada de bits. La cantidad de bits necesaria para codificar todos los caracteres depender, lgicamente, del nmero de caracteres que se deseen

39

Informtica Aplicada. Programacin en Lenguaje C.

codificar. Por ejemplo, con un bit, tan solo se pueden codificar dos caracteres: a uno le correspondera el cdigo 0 y al otro el cdigo 1. No tenemos ms valores de cdigo posibles y, por tanto, no podemos codificar un conjunto mayor de caracteres. Con dos bits, podramos codificar cuatro caracteres; tantos como combinaciones posibles hay con esos dos dgitos binarios: 00, 01, 10 y 11. En general diremos que con n bits seremos capaces de codificar hasta un total de conjunto de longitud caracteres. Y, al revs, si necesitamos codificar un caracteres, necesitaremos una secuencia de bits de . Habitualmente, para un cdigo de representacin de que verifique esta desigualdad.

caracteres se tomar el menor

Una vez decidido el cardinal del conjunto de caracteres que se desea codificar, y tomado por tanto como longitud del cdigo el menor nmero de bits necesarios para lograr asignar un valor de cdigo a cada carcter, el resto del trabajo de creacin del cdigo ser asignar a cada carcter codificado un valor numrico binario codificado con tantos ceros o unos como indique la longitud del cdigo; evidentemente, como en cualquier cdigo, deberemos asignar a cada carcter un valor numrico diferente. Desde luego, se hace necesario lograr universalizar los cdigos, y que el mayor nmeros de mquinas y dispositivos trabajen con la misma codificacin, para lograr un mnimo de entendimiento entre ellas y entre mquinas y perifricos. Para eso surgen los cdigos normalizados de mbito internacional. De entre los diferentes cdigos normalizados vlidos, sealamos aqu el Cdigo ASCII (American Standard Code for Information Interchange). Es un cdigo ampliamente utilizado. Est definido para una longitud de cdigo longitud (es decir, puede codificar hasta 128 caracteres distintos), aunque existe una versin del ASCII de que dobla el nmero de caracteres que se pueden codificar (hasta 256) y que ha permitido introducir un gran nmero de caracteres grficos.

40

Captulo 3. Codificacin interna de la informacin.

En el cdigo ASCII el carcter A tiene el valor decimal 65 (en hexadecimal 41; 0100 0001 en binario), y consecutivamente, hasta el carcter Z (valor decimal 90 en hexadecimal 5A; 0101 1010 en binario), van ordenados alfabticamente, todas las letras maysculas. El alfabeto en minsculas comienza un poco ms adelante, con el cdigo decimal 97 (61 en hexadecimal; 0110 0001 en binario) para la a minscula. Las letras y tienen su cdigo fuera de esta secuencia ordenada. Esta circunstancia trae no pocos problemas en la programacin de aplicaciones de ordenacin o de manejo de texto. Los caracteres numricos comienzan con el valor decimal 48 (30 en hexadecimal) para el carcter 0, y luego, consecutivos, estn codificados los restantes nueve guarismos de la base diez. Es fcil encontrar una tabla con los valores del cdigo ASCII. Quiz puede usted hacer la sencillo tarea de buscar esa tabla y comparar las codificaciones de las letras maysculas y minsculas. Advierte alguna relacin entre ellas? En qu se diferencian las maysculas y sus correspondientes minsculas?

Representacin o Codificacin Interna de la Informacin.


La codificacin de Entrada/Salida no es til para realizar operaciones aritmticas. En ese caso resulta mucho ms conveniente que los valores numricos queden almacenados en su valor codificado en base dos. Por ejemplo, utilizando el cdigo ASCII, el valor numrico 491 queda codificado como 0110100 0111001 0110001. (34 39 31, en hexadecimal: puede buscarlos en una tabla ASCII) Ese mismo valor, almacenado con 16 bits, en su valor numrico, toma el cdigo 0000000111101011 (desarrolle la expansin de este nmero, pasndolo a base 10, si quiere comprobarlo). No resulta mejor cdigo nicamente porque requiere menos dgitos (se adquiere mayor compactacin), sino

41

Informtica Aplicada. Programacin en Lenguaje C.

tambin porque esa codificacin tiene una significacin inmediatamente relacionada con el valor codificado. Y porque un valor as codificado es ms fcilmente operable desde la ALU que si lo tomamos como una secuencia de caracteres de cdigo arbitrario. Es decir, se logra una mayor adecuacin con la aritmtica. Vamos a ver aqu la forma en que un ordenador codifica los valores numricos enteros, con signo o sin signo. Desde luego, existe tambin una definicin y normativa para la codificacin de valores numricos con decimales, tambin llamados de coma flotante (por ejemplo, la normativa IEEE 754), pero no nos vamos a detener en ella.

Enteros sin signo.


Para un entero sin signo, tomamos como cdigo el valor de ese entero expresado en base binaria. Sin ms. Por ejemplo, para codificar el valor numrico tomamos el cdigo 10101111. Adems de saber cmo se codifica el entero, ser necesario conocer el rango de valores codificables. Y eso estar en funcin del nmero de bits que se emplearn para la codificacin. Si tomramos un byte para codificar valores enteros sin signo, entonces podramos codificar hasta un total de 256 valores. Tal y como se ha definido el cdigo en este epgrafe, es inmediato ver que los valores codificados son los comprendidos entre el 0 (cdigo 00000000) y el 255 (cdigo 11111111), ambos incluidos. Si tomramos dos bytes para codificar (16 bits), el rango de valores con ocho bits

codificados ira desde el valor 0 hasta el valor 65.535 (en hexadecimal FFFF). Y si tomramos cuatro bytes (32 bits) el valor mximo posible a codificar sera, en hexadecimal, el FFFFFFFF que, en base 10 es el nmero 4.294.967.295.

42

Captulo 3. Codificacin interna de la informacin.

Evidentemente, cuantos ms bytes se empleen, mayor cantidad de enteros se podrn codificar y ms alto ser el valor numrico codificado. En general, el rango de enteros sin signo codificados con comprendido entre 0 y . bits ser el

Enteros con signo.


Hay diversas formas de codificar un valor numrico con signo. Vamos a ver aqu una de ellas, la que se emplea en los PCs de uso habitual. El modo de codificar enteros con signo es el siguiente: 1. El bit ms significativo no es un dgito numrico, sino el signo. Se pone a 0 si el entero codificado es positivo; se pone a 1 si el entero codificado es negativo. 2. Los restantes bits se emplean para la codificacin del valor numrico. Si el entero es positivo, se codifica en esos bits ese valor numrico en base binaria. Si el entero es negativo, entonces lo que se codifica en esos bits es el complemento a la base del valor absoluto del valor codificado. Si queremos saber entonces cmo queda codificado, con un byte, el valor numrico -75, debemos hacer los siguientes clculos: El bit ms significativo ser 1, porque el entero a codificar es negativo. El cdigo binario del valor absoluto del nmero es 1001011 (siete dgitos, que son los que nos quedan disponibles). El complemento a la base menos uno de ese valor es 0110100 (se calcula invirtiendo todos los dgitos: de 0 a 1 y de 1 a 0), y el complemento a la base ser entonces 0110101 (recurdese la igualdad 2.6.). Por tanto la representacin interna de ese valor ser 10110101, que en base hexadecimal queda B5. Si hubisemos codificado el entero con dos bytes entonces el resultado final del cdigo sera FFB5.

43

Informtica Aplicada. Programacin en Lenguaje C.

El rango de valores codificados cambia con respecto al presentado antes con los enteros sin signo, pero la cantidad de valores codificados es la misma en el caso de enteros con signo y enteros sin signo. Ahora el rango de valores queda centrado en el cero: toma la mitad de los cdigos para valores negativos y la otra mitad para valores positivos ms el cero. Por ejemplo, con un byte se codifican los enteros comprendidos entre -128 y +127. Con dos bytes se pueden codificar los enteros comprendidos entre -32.768 y + 32.767. Y con cuatro bytes el rango de valores codificados va desde el -2.147.483.648 hasta el +2.147.483.647. En general, el rango de valores codificados con comprendido entre y . bits ser el

Recapitulacin.
En este captulo hemos visto cmo se codifica la informacin dentro del ordenador. Hemos conocido primero cmo se trasfiere la informacin desde los perifricos hacia la CPU (cdigo de E/S). Y cmo se codifica la informacin una vez sta ya ha sido introducida en el ordenador: cmo se almacena en la memoria principal o en los registros internos del ordenador; especialmente nos hemos centrado en la codificacin de los enteros. En el epgrafe siguiente se muestran muchos valores negativos codificados tal y como los almacena el ordenador. Despus de terminar el estudio de este captulo es conveniente practicar y obtener la codificacin interna de diferentes valores tomados de forma aleatoria.

Ejercicios.
El mejor modo para llegar a manejar la tcnica de codificacin de los enteros es la prctica. Queda recogida, en este ltimo epgrafe, la

44

Captulo 3. Codificacin interna de la informacin.

codificacin de diferentes valores numricos, para que se pueda practicar y verificar los resultados obtenidos. Todos ellos son valores negativos, y todos ellos codificados con dos bytes (16 bits, 4 dgitos hexadecimales).

-128 -124 -120 -116 -112 -108 -104 -100 -96 -92 -88 -84 -80 -76 -72 -68 -64 -60 -56 -52 -48 -44 -40 -36 -32 -28 -24 -20 -16 -12

FF80 FF84 FF88 FF8C FF90 FF94 FF98 FF9C FFA0 FFA4 FFA8 FFAC FFB0 FFB4 FFB8 FFBC FFC0 FFC4 FFC8 FFCC FFD0 FFD4 FFD8 FFDC FFE0 FFE4 FFE8 FFEC FFF0 FFF4

-127 -123 -119 -115 -111 -107 -103 -99 -95 -91 -87 -83 -79 -75 -71 -67 -63 -59 -55 -51 -47 -43 -39 -35 -31 -27 -23 -19 -15 -11

FF81 FF85 FF89 FF8D FF91 FF95 FF99 FF9D FFA1 FFA5 FFA9 FFAD FFB1 FFB5 FFB9 FFBD FFC1 FFC5 FFC9 FFCD FFD1 FFD5 FFD9 FFDD FFE1 FFE5 FFE9 FFED FFF1 FFF5

-126 -122 -118 -114 -110 -106 -102 -98 -94 -90 -86 -82 -78 -74 -70 -66 -62 -58 -54 -50 -46 -42 -38 -34 -30 -26 -22 -18 -14 -10

FF82 FF86 FF8A FF8E FF92 FF96 FF9A FF9E FFA2 FFA6 FFAA FFAE FFB2 FFB6 FFBA FFBE FFC2 FFC6 FFCA FFCE FFD2 FFD6 FFDA FFDE FFE2 FFE6 FFEA FFEE FFF2 FFF6

-125 -121 -117 -113 -109 -105 -101 -97 -93 -89 -85 -81 -77 -73 -69 -65 -61 -57 -53 -49 -45 -41 -37 -33 -29 -25 -21 -17 -13 -9

FF83 FF87 FF8B FF8F FF93 FF97 FF9B FF9F FFA3 FFA7 FFAB FFAF FFB3 FFB7 FFBB FFBF FFC3 FFC7 FFCB FFCF FFD3 FFD7 FFDB FFDF FFE3 FFE7 FFEB FFEF FFF3 FFF7

45

Informtica Aplicada. Programacin en Lenguaje C.

-8 -4

FFF8 FFFC

-7 -3

FFF9 FFFD

-6 -2

FFFA FFFE

-5 -1

FFFB FFFF

A modo de ejemplo, mostramos los pasos a seguir para llegar a la codificacin interna, en hexadecimal, a partir del valor entero.

3.1.

Codificacin, con 2 bytes, del valor entero

El bit ms significativo estar a 1, porque el entero es negativo. Los restantes 15 bits codifican el valor absoluto del entero. Su valor en binario (con 15 dgitos binarios) es complemento a la base menos uno: y su complemento a la base: . El cdigo del entero en la representacin interna del ordenador ser, al aadirle delante el bit del signo, 1111 1111 1101 0001, que en hexadecimal ser FFD1. . Su

3.2.

Codificacin, con 2 bytes, del valor entero

El bit ms significativo estar a 1, porque el entero es negativo. Los restantes 15 bits codifican el valor absoluto del entero. Su valor en binario (con 15 dgitos binarios) es: complemento a la base menos uno: y su complemento a la base: . El cdigo del entero en la representacin interna del ordenador ser, en hexadecimal, al aadirle el bit del signo, FFFF. . Su

46

Captulo 3. Codificacin interna de la informacin.

3.3.

Otras formulaciones que ayudan a practicar y as entender la codificacin interna de los enteros:

Indicar

el

valor

numrico

que

queda

codificado

con

89D4

(codificacin en 16 bits) Si un entero queda codificado con AB (suponiendo una codificacin en 16 bits), indique cmo quedara codificado su valor cambiado de signo. Supongamos una codificacin en 16 bits. Indique cules de las siguientes codificaciones de enteros con signo corresponden a enteros positivos, y cules a enteros negativos: ABCD, 7FFF, A0, FFF0. Ordene los nmeros codificados en la pregunta anterior de menor a mayor.

47

Informtica Aplicada. Programacin en Lenguaje C.

48

CAPTULO 4
LENGUAJE C.
Presentamos en este captulo una primera vista de la programacin en lenguaje C. El objetivo ahora es mostrar los conceptos bsicos de un entorno de programacin, y redactar, con el entorno que cada uno quiera, un primer programa en C, que nos servir para conocer las partes principales de un programa.

Introduccin.
Los lenguajes de programacin estn especialmente diseados para programar computadoras. Sus caractersticas fundamentales son: 1. Son independientes de la arquitectura fsica del ordenador. Los lenguajes estn, adems, normalizados, de forma que queda garantizada la portabilidad de los programas escritos en esos lenguajes: un programa escrito en una mquina puede utilizarse en otra mquina distinta.

Informtica Aplicada. Programacin en Lenguaje C.

2. Normalmente un mandato o sentencia en un lenguaje de alto nivel da lugar, al ser introducido, a varias instrucciones en lenguaje mquina. 3. Utilizan notaciones cercanas a las habituales, con sentencias y frases semejantes al lenguaje matemtico o al lenguaje natural. El lenguaje C se dise en 1969. El lenguaje, su sintaxis y su semntica, as como el primer compilador de C fueron diseados y creados por Dennis M. Ritchie y Ken Thompson, en los laboratorios Bell. Ms tarde, en 1983, se defini el estndar ANSI C (que es el que aqu presentaremos). El lenguaje C tiene muy pocas reglas sintcticas, sencillas de aprender. Su lxico es muy reducido: tan solo 32 palabras. A menudo se le llama lenguaje de medio nivel, ms prximo al cdigo mquina que muchos lenguajes de ms alto nivel. Es un lenguaje apreciado en la comunidad cientfica por su probada eficiencia. Es el lenguaje de programacin ms popular para crear software de sistemas, aunque tambin se utiliza para implementar aplicaciones. Permite el uso del lenguaje ensamblador en partes del cdigo, trabaja a nivel de bit, y permite modificar los datos con operadores que manipulan bit a bit la informacin. Tambin se puede acceder a las diferentes posiciones de memoria conociendo su direccin. El lenguaje C es un lenguaje del paradigma imperativo, estructurado. Permite con facilidad la programacin modular, creando unidades que pueden compilarse de forma independiente, que pueden posteriormente enlazarse. As, se crean funciones o procedimientos que se pueden compilar y almacenar, creando bibliotecas de cdigo ya editado y compilado que resuelve distintas operaciones. Cada programador puede disear sus propias bibliotecas, que simplifican luego considerablemente el trabajo futuro. El ANSI C posee una amplia coleccin de bibliotecas de funciones estndar y normalizadas.

50

Captulo 4. Lenguaje C.

Entorno de programacin.
Para realizar la tarea de escribir el cdigo de una aplicacin en un determinado lenguaje, y poder luego compilar y obtener un programa que realiza la tarea planteada, se dispone de lo que se denomina un entorno de programacin. Un entorno de programacin es un conjunto de programas necesarios para construir, a su vez, otros programas. Un entorno de programacin incluye editores de texto, compiladores, archivos de biblioteca, enlazadores y depuradores (ya se ir conociendo el significado de todos estos trminos). Gracias a Dios existen entornos de programacin integrados (genricamente llamados IDE, acrnimo en ingls de Integrated Development Environment), de forma que en una sola aplicacin quedan reunidos todos estos programas. Ejemplos de entornos integrados de programacin en C son el programa Microsoft Visual C++, o el Turbo C++ de Borland. Otros entornos pueden obtenerse libremente a travs de internet. Por ejemplo, el entorno de programacin Dev-C++ (disponible en mltiples enlaces), CodeLite (http://codelite.org/). En el desarrollo de las clases de la asignatura se usar unos de esos entornos de libre distribucin. Para conocer el uso de una herramienta de programacin concreta, lo mejor es consultar la documentacin que, para cada una de ellas, suele haber disponible tambin de forma gratuita, a travs de Internet. No se presentar en este manual ningn IDE concreto. Un editor es un programa que permite construir ficheros de caracteres, que el programador introduce a travs del teclado. Un programa no es ms que archivo de texto. El programa editado en el lenguaje de programacin se llama fichero fuente. Algunos de los editores facilitan el correcto empleo de un determinado lenguaje de programacin, y advierten de inmediato la insercin de una palabra clave, o de la presencia de un error sintctico, marcando el texto de distintas formas.

51

Informtica Aplicada. Programacin en Lenguaje C.

Un compilador es un programa que compila, es decir, genera ficheros objeto que entiende el ordenador. Un archivo objeto todava no es una archivo ejecutable. El entorno ofrece tambin al programador un conjunto de archivos para incluir o archivos de cabecera. Esos archivos suelen incluir abundantes parmetros que hacen referencia a diferentes caractersticas de la mquina sobre la que se est trabajando. As, el mismo programa en lenguaje de alto nivel, compilado en mquinas diferentes, logra archivos ejecutables distintos. Es decir, el mismo cdigo fuente es as portable y vlido para mquinas diferentes. Otros archivos son los archivos de biblioteca. Son programas previamente compilados que realizan funciones especficas. Suele suceder que determinados bloques de cdigo se deban escribir en diferentes programas. Ciertas partes que son ya conocidas porque son comunes a la mayor parte de los programas estn ya escritas y vienen recogidas y agrupadas en archivos que llamamos bibliotecas. Ejemplos de estas funciones son muchas matemticas (trigonomtricas, o numricas,) o funciones de entrada de datos desde teclado o de salida de la informacin del programa por pantalla (cfr. Captulo 8 de este manual). Desde luego, para hacer uso de una funcin predefinida, es necesario conocer su existencia y tener localizada la biblioteca donde est pre-compilada; eso es parte del aprendizaje de un lenguaje de programacin, aunque tambin se disponen de grandes ndices de funciones, de fcil acceso para su consulta. Al compilar un programa generamos un archivo objeto. Habitualmente los programas que compilemos harn uso de algunas funciones de biblioteca; en ese caso, el archivo objeto no es an un fichero ejecutable, puesto que le falta aadir el cdigo de esas funciones. Un entorno de programacin que tenga definidas bibliotecas necesitar tambin un enlazador o linkador (perdn por esa palabra tan horrible) que realice la tarea de juntar el archivo objeto con las bibliotecas

52

Captulo 4. Lenguaje C.

empleadas y llegar, as, al cdigo ejecutable. La creacin e implementacin de un programa no suele terminar con este ltimo paso descrito. Con frecuencia se encontrarn errores, bien de compilacin porque haya algn error sintctico o de expresin y manejo del lenguaje; bien de ejecucin, porque el programa no haga exactamente lo que se deseaba. No siempre es sencillo encontrar los errores de nuestros programas; un buen entorno de programacin ofrece al programador algunas herramientas llamadas depuradores, que facilitan esta tarea. Podramos escribir el algoritmo que define la tarea de crear un programa. Ese algoritmo podra tener el aspecto del recogido en el flujograma de la Figura 4.1. En el caso del lenguaje C, el archivo de texto donde se almacena el cdigo tendr un nombre (el que se quiera) y la extensin .cpp (si trabajamos con un entorno de programacin de C++), o .c. Al compilar el fichero fuente (nombre.cpp) se llega al cdigo mquina, con el mismo nombre que el archivo donde est el cdigo fuente, y con la extensin .obj. Casi con toda probabilidad en cdigo fuente har uso de funciones que estn ya definidas y pre-compiladas en las bibliotecas. Ese cdigo
I Escritura del programa fuente (.cpp) Compilacin

Errores de compilacin

No

Obtencin del programa objeto (.obj)

Obtencin del programa ejecutable (.exe) S Errores de ejecucin No Programas objeto del usuario

Enlace

Figura 4.1.: Fases de desarrollo de un programa

Archivos de biblioteca (.lib)

53

Informtica Aplicada. Programacin en Lenguaje C.

pre-compilado est en archivos con la extensin .lib. Con el archivo .obj y los necesarios .lib que se deseen emplear, se procede al linkado o enlazado que genera un fichero ejecutable con la extensin .exe.

Estructura bsica de un programa en C.


Aqu viene escrito un sencillo programa en C. Quiz convenga ponerse ahora delante del ordenador y, con el editor de C en la pantalla, escribir estas lneas y ejecutarlas. #include <stdio.h> /* Este es un programa en C. */ // Imprime un mensaje en la pantalla del ordenador int main(void) { printf(mi primer programa en C); return 0; } Todos los programas en C deben tener ciertos componentes fijos. Vamos a ver los que se han empleado en este primer programa: 1. #include <stdio.h>: Los archivos .h son los archivos de cabecera en C. Con esta lnea de cdigo se indica al compilador que se desea emplear, en el programa redactado, alguna funcin que est declarada en el archivo de biblioteca stdio.h. Esta archivo contiene las declaraciones de una coleccin de programas de entrada y salida por consola (pantalla y teclado). Esta instruccin nos permite utilizar cualquiera de las funciones declaradas en el archivo. Esta lnea de cdigo recoge el nombre del archivo stdio.h, donde estn recogidos todos los prototipos de las funciones de entrada y salida estndar. Todo archivo de cabecera contiene identificadores, constantes, variables globales, macros, prototipos de funciones, etc. Toda lnea que comience por # se llama directiva de

preprocesador. A lo largo del libro se irn viendo diferentes

54

Captulo 4. Lenguaje C.

directivas. 2. main: Es el nombre de una funcin. Es la funcin principal y establece el punto donde comienza la ejecucin del programa. La funcin main es necesaria en cualquier programa de C que desee ejecutar instrucciones. Un cdigo ser ejecutable si y slo si dispone de la funcin main. 3. int main(void): Los parntesis se encuentran siempre despus de un identificador de funcin. Entre ellos se recogen los parmetros que se pasan a la funcin al ser llamada. En este caso, no se recoge ningn parmetro, y entre parntesis se indica el tipo void. Ya se ver ms adelante qu significa esta palabra. Delante del nombre de la funcin principal (main) tambin viene la palabra int, porque la funcin principal que hemos implementado devuelve un valor de tipo entero con signo: en concreto, en nuestro ejemplo, devuelve el valor 0 (instruccin return 0;) 4. /* comentarios */: Smbolos opcionales. Todo lo que se encuentre entre estos dos smbolos son comentarios al programa fuente y no sern ledos por el compilador. Los comentarios no se compilan, y por tanto no son parte del programa; pero son muy necesarios para lograr unos cdigos inteligibles, fcilmente interpretables tiempo despus de que hayan sido redactados y compilados. Es muy conveniente, cuando se realizan tareas de programacin, insertar comentarios con frecuencia que vayan explicando el proceso que se est llevando en cada momento. Un programa bien documentado es un programa que luego se podr entender con facilidad y ser, por tanto, ms fcilmente modificado y mejorado. Tambin se pueden incluir comentarios precedindolos de la doble barra //. En ese caso, el compilador no toma en consideracin lo que est escrito desde la doble barra hasta el final de la presente

55

Informtica Aplicada. Programacin en Lenguaje C.

lnea. 5. ;: Toda sentencia en C termina con el punto y coma. En C, se entiende por sentencia todo lo que tenga, al final, un punto y coma. La lnea antes comentada (#include <stdio.h>) no termina con un punto y coma porque no es una sentencia: es (ya lo hemos dicho) una directiva de preprocesador. 6. {}: Indican el principio y el final de todo bloque de programa. Cualquier conjunto de sentencias que se deseen agrupar, para formar entre ellas una sentencia compuesta o bloque, irn marcadas por un par de llaves: una antes de la primera sentencia a agrupar; la otra, de cierre, despus de la ltima sentencia. Una funcin es un bloque de programa y debe, por tanto, llevarlas a su inicio y a su fin. 7. La sentencia return 0; Como acabamos de definir, la funcin main devuelve un valor de tipo int: por eso hemos escrito, delante del nombre de la funcin, esa palabra. La funcin main es tal que antes de terminar devolver el valor 0 (as lo indica esta sentencia o instruccin). An es demasiado pronto para saber a quin le es devuelto ese valor. Por ahora hay que aprender a hacerlo as. La tarea de aprender a programar exige, en sus primeros pasos, saber fiarse de los manuales y de quien pueda ensearnos. No se puede explicar todo el primer da.

Elementos lxicos.
Entendemos por elemento lxico cualquier palabra vlida en el lenguaje C. Sern elementos lxicos, o palabras vlidas, todas aquellas palabras que formen parte de las palabras reservadas del lenguaje, y todas aquellas palabras que necesitemos generar para la redaccin del programa, de acuerdo con una normativa sencilla. Para crear un identificador (un identificador es un smbolo empleado para representar un objeto dentro de un programa) en el lenguaje C se

56

Captulo 4. Lenguaje C.

usa cualquier secuencia de una o ms letras (de la A a la Z, y de la a a la z, excluida las letras y ), dgitos (del 0 al 9) o smbolo subrayado (_). Los identificadores creados sern palabras vlidas en nuestro programa en C. Con ellos podemos dar nombre a variables, constantes, tipos de dato, nombres de funciones o procedimientos, etc. Tambin las palabras propias del lenguaje C son identificadores; estas palabras se llaman palabras clave o palabras reservadas. Adems de la restriccin en el uso de caracteres vlidos para crear identificadores, existen otras reglas bsicas para su creacin en el lenguaje C. Estas reglas bsicas (algunas ya han quedado dichas, pero las repetimos para mostrar en este elenco todas las reglas juntas) son las siguientes: 1. Estn formadas por los caracteres de tipo alfabtico (de la A a la Z, y de la a a la z), caracteres de tipo dgito (del 0 al 9) y el signo subrayado (algunos lo llaman guin bajo: _). No se admite nuestra (ni la mayscula), y tampoco se aceptan aquellos caracteres acentuados, con diresis, o con cualquier otra marca. 2. Debe comenzar por una letra del alfabeto o por el carcter subrayado. Un identificador no puede comenzar por un dgito. 3. El compilador slo reconoce los primeros 32 caracteres de un identificador, pero ste puede tener cualquier otro tamao mayor. Aunque no es nada habitual generar identificadores tan largos, si alguna vez as se hace hay que evitar que dos de ellos tengan iguales los 32 primeros caracteres, porque entonces para el compilador ambos identificadores sern el mismo. 4. Las letras de los identificadores pueden ser maysculas y

minsculas. El compilador distingue entre unas y otras, y dos identificadores que se lean igual y que se diferencien nicamente en que una de sus letras es mayscula en uno y minscula en otro, son distintos.

57

Informtica Aplicada. Programacin en Lenguaje C.

5. Un identificador no puede deletrearse igual y tener el mismo tipo de letra (mayscula o minscula) que una palabra reservada o que una funcin definida en una librera que se haya incluido en el programa mediante una sentencia include. Las palabras reservadas, o palabras clave, son identificadores

predefinidos que tienen un significado especial para el compilador de C. Slo se pueden usar en la forma en que han sido definidos. El conjunto de palabras clave o reservadas (que siempre van en minscula) en ANSI C es muy reducido (un total de 32) y son las siguientes: auto break case char const continue default do double else enum extern float for (goto) if int long register return short signed sizeof static struct switch typedef union unsigned void volatile while

A lo largo del manual se ver el significado de cada una de ellas. La palabra goto viene recogida entre parntesis porque, aunque es una palabra reservada en C y su uso es sintcticamente correcto, de hecho no es una palabra permitida en un paradigma de programacin estructurado como es el paradigma del lenguaje C. Esta palabra ha quedado como reliquia de las primeras versiones del C. De la misma manera, no es estrictamente necesaria (como veremos) aunque s puede utilizarse y es buena prctica hacerlo, la palabra signed.

Sentencias simples y sentencias compuestas.


Una sentencia simple es cualquier expresin vlida en la sintaxis de C que termine con el carcter de punto y coma. Sentencia compuesta es una sentencia formada por una o varias sentencias simples. La sentencia simple queda definida cuando el programador termina una expresin vlida en C, con un punto y coma. La sentencia compuesta se

58

Captulo 4. Lenguaje C.

inicia con una llave de apertura ({) y se termina con una llave de clausura (}).

Errores y depuracin.
No es extrao que, al terminar de redactar el cdigo de un programa, al iniciar la compilacin, el compilador deba abortar su proceso y avisar de que existen errores. El compilador ofrece algunos mensajes que clarifican frecuentemente el motivo del error, y la correccin de esos errores no comporta habitualmente demasiada dificultad. A esos errores sintcticos los llamamos errores de compilacin. Ejemplo de estos errores pueden ser que se haya olvidado terminar una sentencia con el punto y coma, o que falte una llave de cierre de bloque de sentencias compuestas, o sobre un parntesis, o se emplee un identificador mal construido Otras veces, el compilador no haya error sintctico alguno, y compila correctamente el programa, pero luego, en la ejecucin, se producen errores que acaban por abortar el proceso. A esos errores los llamamos errores de ejecucin. Un clsico ejemplo de este tipo de errores es forzar al ordenador a realizar una divisin por cero, o acceder a un espacio de memoria para el que no estamos autorizados. Esos errores tambin suelen ser sencillos de encontrar, aunque a veces, como no son debidos a fallos sintcticos ni de codificacin del programa sino que pueden estar ocasionados por el valor que en un momento concreto adquiera una variable, no siempre son fcilmente identificables, y en esos casos puede ser necesario utilizar los depuradores que muchos entornos de programacin ofrecen. Y puede ocurrir tambin que el cdigo no tenga errores sintcticos, y por tanto el compilador termine su tarea y genere un ejecutable; que el programa se ejecute sin contratiempo alguno, porque en ningn caso se llega a un error de ejecucin; pero que el resultado final no sea el

59

Informtica Aplicada. Programacin en Lenguaje C.

esperado. Todo est sintcticamente bien escrito, sin errores de compilacin ni de ejecucin, pero hay errores en el algoritmo que pretende resolver el problema que nos ocupa. Esos errores pueden ser ocasionados sencillamente por una errata a la hora de escribir el cdigo, que no genera un error sintctico, ni aborta la ejecucin: por ejemplo, teclear indebidamente el operador suma (+) cuando el que corresponda era el operador resta (-). A veces, sin embargo, el gazapo no es fcil de encontrar. No hemos avanzado lo suficiente como para poner algn ejemplo. Cada uno descubrir los suyos propios en su itinerario de aprendizaje del lenguaje: es cuestin de tiempo encontrarse con esas trampas. Todo error requiere una modificacin del programa, y una nueva compilacin. No todos los errores aparecen de inmediato, y no es extrao que surjan despus de muchas ejecuciones.

Estndar C89, Estndar C90 y Estndar C99.


El ANSI C suele tambin llamarse C89. Fue el primer estndar de C que se cre, adoptado por el American National Standards Institute. El nombre que se le dio a este estndar fue el ANSI X3.159-1989. C90 fue la primera versin del C Estndar, adoptado por el International Organization for Standardization. El nombre que se le dio a este nuevo estndar fue el ISO/IEC 9899:1990. A efectos prcticos, ambos estndares son el mismo lenguaje: las modificaciones introducidas por C90 respecto a C89 son mnimas. En marzo de 2000 se adopt un tercer estndar: el ISO/IEC 9899:1999. A este estndar se le conoce como C99, y es el estndar actual para el lenguaje de programacin C. Casi todos los compiladores que se encuentran en actualmente en el mercado compilan para C99, aunque todos ellos permiten hacer restricciones para que nicamente trabaje con las especificaciones del C89 del C90.

60

Captulo 4. Lenguaje C.

Entre otras muchas cosas, el lenguaje C99 introduce algunas nuevas palabras clave en el lenguaje. Estas palabras son: restrict, _Bool, _Complex e _Imaginary. Ninguna de ellas es soportada por C++, y, desde luego, tampoco por C90. Adems, incluye la nueva palabra inline, que s est soportada en C++. A lo largo de este manual se presenta, salvo algunas contadas salvedades, el estndar C90. En frecuentes ocasiones quedarn tambin indicadas en el manual, con epgrafe aparte, algunas nuevas aportaciones del estndar C99, pero no siempre. Tngase en cuenta que el lenguaje Estndar C90 es casi un subconjunto perfecto del lenguaje orientado a objetos C++, y que bastantes de las novedades que incorpora el estndar C99 pueden tambin transportarse en un programa que se desee compilar con un compilador de C++; pero hay otras nuevas aportaciones del C99 que no estn soportadas por el C++: A stas les he dedicado menor atencin en este manual, o simplemente no han quedado citadas.

Recapitulacin.
En este captulo hemos introducido los conceptos bsicos iniciales para poder comenzar a trabajar en la programacin con el lenguaje C. Hemos presentado el entorno habitual de programacin y hemos visto un primer programa en C (sencillo, desde luego) que nos ha permitido mostrar las partes bsicas del cdigo de un programa: las directivas de preprocesador, los comentarios, la funcin principal, las sentencias (simples o compuestas) y las llaves que agrupan sentencias. Y hemos aprendido las reglas bsicas de creacin de identificadores.

61

Informtica Aplicada. Programacin en Lenguaje C.

62

CAPTULO 5
ALGORITMOS.
El objetivo de este captulo es presentar el concepto de algoritmo, aprender algunas y herramientas plantear de creacin de pseudocdigo para afianzar de los programacin, suficientes ejercicios

conceptos introducidos. Es importante comprender y asimilar bien los contenidos de este captulo: se trata de ofrecer las herramientas bsicas para lograr expresar un procedimiento que pueda entender un ordenador; aprender cmo resolver un problema concreto mediante una secuencia ordenada y finita de instrucciones sencillas y precisas. Si ante un problema planteado logramos expresar el camino de la solucin de esta forma, entonces la tarea de aprender un lenguaje de programacin se convierte en algo sencillo y, hasta cierto punto, trivial. Una vez se sabe qu se ha de decir al ordenador, slo resta la tarea de expresarlo en un lenguaje cualquiera. Las principales referencias utilizadas para la confeccin de este captulo

Informtica Aplicada. Programacin en Lenguaje C.

han sido: El arte de programar ordenadores. Volumen I: Algoritmos Fundamentales Donald E. Knuth Editorial Revert, S.A., 1985 Introduccin a la Informtica. 3 Edicin Alberto Prieto E., Antonio Lloris R., Juan Carlos Torres C. Editorial Mc Graw Hill, 2006

Concepto de Algoritmo.
La nocin de algoritmo es bsica en la programacin de ordenadores. El diccionario de la Real Academia Espaola lo define como conjunto ordenado y finito de operaciones que permite hallar la solucin de un problema. Otra definicin podra ser: procedimiento no ambiguo que resuelve un problema, entendiendo por procedimiento (informtico) una secuencia de operaciones bien definida, cada una de las cuales requiere una cantidad finita de memoria y se realiza en un tiempo finito. Hay que tener en cuenta que la arquitectura de un ordenador permite la realizacin de un limitado conjunto de operaciones, todas ellas muy sencillas, tales como sumar, restar, transferir datos, etc. O expresamos los procedimientos en forma de instrucciones sencillas (es decir, no complejas) y simples (es decir, no compuestas), o no lograremos luego indicarle al ordenador (programarlo) qu rdenes debe ejecutar para alcanzar una solucin. No todos los mtodos de solucin de un problema son vlidos para ser utilizados por un ordenador. Para que un procedimiento pueda ser luego convertido en un programa ejecutable por una computadora, debe verificar las siguientes propiedades: 1. Debe finalizar tras un nmero finito de pasos. Vale la pena remarcar la idea de que los pasos deben ser, efectivamente, muy finitos. 2. Cada uno de sus pasos debe definirse de un modo preciso. Las

64

Captulo 5. Algoritmos.

acciones a realizar han de estar especificadas en cada caso de forma rigurosa y sin ambigedad. 3. Puede tener varias entradas, o ninguna. Sin embargo, al menos debe tener una salida: el resultado que se pretende obtener. Al hablar de entradas o de salidas nos referimos a la informacin (en forma de datos) que se le debe suministrar al algoritmo para su ejecucin, y la informacin que finalmente ofrece como resultado del proceso definido. 4. Cada una de las operaciones a realizar debe ser lo bastante bsica como para poder ser efectuada por una persona con papel y lpiz, de modo exacto en un lapso de tiempo finito. Cuando un procedimiento no ambiguo que resuelve un determinado problema verifica adems estas cuatro propiedades o condiciones, entonces algoritmo. De acuerdo con Knuth nos quedamos con la siguiente definicin de algoritmo: una secuencia finita de instrucciones, reglas o pasos que describen de forma precisa las operaciones que un ordenador debe realizar para llevar a cabo una tarea en un tiempo finito. El algoritmo que ha de seguirse para alcanzar un resultado buscando no es nico. Habitualmente habr muchos mtodos o procedimientos distintos para alcanzar la solucin buscada. Cul de ellos sea mejor que otros depender de muchos factores. En la prctica no slo queremos algoritmos: queremos buenos algoritmos. Un criterio de bondad frecuentemente utilizado es el tiempo que toma la ejecucin de las instrucciones del algoritmo. Otro criterio es la cantidad de recursos (principalmente de memoria) que demanda la ejecucin del algoritmo. Veamos un ejemplo. Vamos a definir un algoritmo para obtener el factorial de un entero. Es evidente que hay diversas formas de calcular diremos, efectivamente, que ese procedimiento es un

65

Informtica Aplicada. Programacin en Lenguaje C.

ese valor. Aqu vamos a disear un algoritmo posible. El factorial de un nmero se define como el producto de todos los enteros positivos igual o menores que ese nmero del que queremos calcular su factorial: .

Un algoritmo vlido para el clculo del factorial de un entero podra ser el siguiente: Algoritmo F (clculo del factorial de un entero). Dado un entero positivo , calcular su factorial. F1 F2 [Inicializar] Mientras que [Operaciones]: . Repetir . . El valor de factorial es

F4

[Mostrar resultado]

Cada paso del algoritmo lo empezamos con una frase (recogida entre corchetes) que resumen de forma breve el contenido principal de ese paso. Esas frases sern muy tiles para definir correctamente el diagrama de flujo del algoritmo. El modo en que se construye un flujograma lo veremos en el prximo epgrafe de este captulo. Probemos si el algoritmo, tal y como est escrito, ofrece como resultado el valor factorial del valor de entrada comencemos el proceso: F1 F2 F2 F2 F2 F2 F2 F3 . , es distinto de cero. , . , es distinto de cero. , . , es distinto de cero. , . , es distinto de cero. , . , es distinto de cero. , . , Termina el proceso. Resultado es . Supongamos y

Resultado que es, efectivamente, el buscado.

66

Captulo 5. Algoritmos.

Representacin de algoritmos.
No se trata en este captulo de presentar una tcnica para dar solucin a cualquier problema de mundo real. El programador debe conocer el contexto del problema que aborda, y dominar las herramientas matemticas o de otra ndole que se requieran en cada caso. La experiencia juega un gran papel a la hora de disear un nuevo algoritmo. Y la experiencia no se adquiere slo leyendo un manual: es cuestin de enfrentarse a un reto tras otro, hasta adquirir oficio. Si usted no adquiere ese oficio no lograr aprender a programar. Quiz se le pueda ayudar con unas clases: pero siempre en estas tareas lo ms eficaz es el esfuerzo y el trabajo personal. Lo que se presenta en este epgrafe son dos formas o mtodos de formulacin o representacin los algoritmos. El primero es mediante la narracin de las tareas que el algoritmo debe ir haciendo. Para facilitar la descripcin es frecuente usar un lenguaje de descripcin de algoritmos o pseudocdigo. No existen reglas fijas para la representacin narrativa de algoritmos. No se exigen tampoco reglas sintcticas estrictas: el inters del pseudocdigo se centra en la secuencia de instrucciones. El algoritmo para el clculo del factorial podra quedar descrito en pseudocdigo de la siguiente forma: 1. 2. 3. Leer [Inicializar variables]: Mientras que Repetir: [Operaciones]: 3.1. 3.2. [Mostrar resultado]: Mostrar Fin

4. 5.

El pseudocdigo expresa en forma de sentencias simples y sencillas todos los pasos que debe ejecutar el algoritmo para su completa

67

Informtica Aplicada. Programacin en Lenguaje C.

realizacin. Adems de las operaciones que se deben ejecutar, usamos algunas palabras tiles para expresar acciones o verificar condiciones: Leer / Mostrar / Si <condicin> entonces [] Sino / Mientras que <condicin> Repetir.

abc

a0

No

Leer a

DECISIN SENTENCIA ENTRADA o SALIDA


C

mcd(a, b)
F

Figura 5.1.: Smbolos usados habitualmente para confeccionar flujogramas.

LLAMADA A PROCEDIMIENTO

COMIENZO TRMINO

El segundo mtodo que vamos a utilizar para representar un algoritmo es mediante diagramas de flujo o flujogramas. Un flujograma es una herramienta grfica que representa un algoritmo. Se compone de una serie de smbolos unidos por flechas. Los smbolos representan acciones, y las flechas el orden de realizacin de las acciones. Cada smbolo tendr, por tanto, al menos una flecha que conduzca a l y una flecha que parta de l. Los distintos smbolos utilizados habitualmente para confeccionar

flujogramas quedan recogidos en la Figura 5.1. Hemos recogido los smbolos que representan las siguientes acciones: sentencia simple (habitualmente asignacin), lectura/escritura, llamada a subrutina o funcin, decisin, comienzo, y fin. Desde luego, existen otros smbolos, que no van a quedar recogidos en este manual.

68

Captulo 5. Algoritmos.

Las acciones de Lectura / Escritura de datos nos forman habitualmente parte de la secuencia de instrucciones del algoritmo. De forma habitual, un programa o una funcin simple puede dividirse en tres partes: entrada de datos proceso de datos muestra o transferencia de datos procesados o resultados. El algoritmo es la parte que describe el proceso, aunque con frecuencia estas tres partes no son fcilmente diferenciadas y se intercalan entre s. En los ejemplos presentados en este captulo recogemos las sentencias de entrada y salida de datos: pero ha de quedar claro que el algoritmo es independiente de estos dos pasos. Y, desde luego, no es necesario que la salida del algoritmo se muestre: es cierto que todo algoritmo obtiene un resultado o salida; pero lo que haga el programa con esa salida obtenida por el algoritmo (mostrarla, almacenarla en memoria, transferirla a otro proceso como nueva entrada,) es cosa que al algoritmo no le importa. El algoritmo para el clculo del factorial representado con un diagrama de flujo queda como se recoge en la Figura 5.2.

Leer

No

Mostrar

Figura 5.2.: Flujograma para el algoritmo de clculo del Factorial de un entero positivo Y aunque an no hemos llegado, en este manual, a presentar las nociones bsicas necesarias para escribir un programa, se puede ver

69

Informtica Aplicada. Programacin en Lenguaje C.

ahora qu aspecto tiene este algoritmo escrito en lenguaje C. No se trata ahora de entenderlo, sino simplemente de ver la forma que toma. El cdigo que ejecutara este proceso tendra un aspecto como el que sigue (la variable almacena inicialmente el valor numrico del que se quiere conocer el factorial): int main(void) { long Fact = 1; // Inicializar variables. unsigned short n; printf(Introduzca el valor de n ... ); scanf(%hu, &n); while(n != 0) // Mientas que n sea distinto de cero... { Fact *= n; n--; } return 0; } Al finalizar estas instrucciones, la variable variable . almacena el valor del

factorial del valor introducido por teclado y codificado inicialmente en la

Reglas bsicas de la programacin estructurada para la construccin de algoritmos.


Para la construccin de un algoritmo (en el paradigma de la programacin estructurada: este concepto de paradigma se ver ms adelante: cfr. captulo 6), dispondremos de las siguientes estructuras de ejecucin de sentencias: 1. Sentencia simple: Entendemos por sentencia simple aquella que puede representarse mediante una caja de sentencia o una caja de entrada o salida. Por ejemplo, una asignacin de una operacin en una variable, o solicitar un nuevo valor de entrada. 2. Sentencia compuesta: Es aquella que est formada por la concatenacin ordenada de dos o ms sentencias simples. 3. Bifurcacin abierta: Realizada mediante una estructura de

70

Captulo 5. Algoritmos.

decisin, tendremos una bifurcacin abierta cuando la ejecucin de una sentencia (simple o compuesta) quede condicionada segn la evaluacin de una expresin lgica. 4. Bifurcacin cerrada: De nuevo mediante una estructura de decisin, la bifurcacin cerrada decide, en el momento de la ejecucin del algoritmo, cul, de entre dos sentencias, se ejecutar. En el esquema de una bifurcacin cerrada aparecen dos sentencias: pero de hecho slo una de ellas puede ser ejecutada. Entendemos, por tanto, que una bifurcacin cerrada se toma como una sentencia simple. 5. Estructuras de repeticin: una sentencia se ejecutar mientras

S1 S1 S2
F F

C S1 S1

C S2

Sentencias Simple y Compuesta


I

Bifurcacin Simple y Compuesta


I I

C S1
F

S1

S1

C
F

C S2 (c)
F

(a)

(b) Estructuras de repeticin

Figura 5.3.: Estructuras bsicas de la programacin estructurada

71

Informtica Aplicada. Programacin en Lenguaje C.

que una expresin concreta se evale como verdadera. Hay tres formas de estructuras de repeticin, como puede verse en la Figura 5.3.: las tres son muy parecidas, y la principal diferencia entre ellas es la cantidad de veces que se ejecuta la sentencia iterada o repetida. Estas estructuras se conocen como estructuras de control. Una estructura de control no supone una nueva instruccin a ejecutar: se limita a determinar cules instrucciones se van a ejecutar en cada momento. Una estructura de control tiene capacidad para alterar el orden secuencial de las sentencias del algoritmo. Una bifurcacin es una estructura de control que permite discriminar qu instrucciones se ejecutan en un punto dado del algoritmo, y cules otras instrucciones quedan sin ejecutar. Una bifurcacin posee siempre un argumento, que es una expresin lgica (es decir, una expresin que se evala como verdadera o falsa, sin ningn valor intermedio posible). Dependiendo del valor de la expresin lgica se ejecutan las acciones que estn en uno u otro camino, a partir de la decisin. Una decisin permite, por tanto, bifurcar en dos caminos el flujo de acciones. El otro tipo de estructuras de control son las iteraciones. Una iteracin es una estructura de control que permite ejecutar un segmento de nuestro proceso o algoritmo un determinado nmero de veces: una, varias, e incluso ninguna. De nuevo existe una expresin lgica que determina cundo se puede ejecutar el segmento de instrucciones y cundo se debe abandonar ya esa ejecucin. A esa expresin la llamamos expresin de control. Como hemos visto, podemos distinguir tres tipos de estructuras de control iterativas: 1. Ciclo condicional con comprobacin al comienzo. Mientras que se cumpla una determinada condicin (formulada en la expresin de control) se ha de repetir el bloque de instrucciones

72

Captulo 5. Algoritmos.

controlado por este ciclo condicional. 2. Ciclo condicional con comprobacin al final. Repetir la ejecucin de un determinado bloque de instrucciones hasta que deje de cumplirse una determinada condicin (formulada en la expresin de control). 3. Un hibrido entre estos dos primeros es aquella estructura de control en que parte de las sentencias iteradas se ejecutan antes de la evaluacin de la condicin de permanencia y parte se ejecutan despus de la evaluacin de la condicin. 4. Tambin podemos disponer de una estructura de control iterada controlada por variable. Una estructura de control as definida no introduce un nuevo esquema de flujograma, sino un modo distinto de interpretar un proceso iterado. En este caso no se cuenta con una expresin de control sino con una variable de control. A esta variable se le asigna un valor inicial, y se le van asignando valores sucesivos, uno distinto en cada nueva ejecucin del bloque de instrucciones iterado, hasta alcanzar un valor final. Es una estructura que se emplea preferentemente cuando se debe ejecutar un bloque de sentencias un nmero de veces concreto y conocido de antemano. Para el empleo de estas estructuras de control iteradas podemos emplear expresiones de pseudocdigo que faciliten la introduccin y uso de esas estructuras: Para el caso de la iteracin con comprobacin de condicin de permanencia al comienzo podemos usar la expresin o forma Mientras <condicin> Repetir <operaciones>. Para el segundo caso (iteracin con comprobacin de condicin de permanencia al final) podemos usar la expresin o forma Repetir: <operaciones> Hasta <condicin>. Para el caso de la iteracin controlada por variable podemos usar la

73

Informtica Aplicada. Programacin en Lenguaje C.

expresin o forma Para <variable de control igual a su valor inicial> Hasta <aqu se indica el valor mximo de la variable de control> Repetir <operaciones>. Por ejemplo: Para <operaciones>. Con todas estas estructuras, las reglas bsicas de la programacin estructurada son las siguientes: 1. Toda sentencia simple puede ser sustituida por una sentencia compuesta, concatenacin de varias simples. 2. Toda sentencia simple puede ser sustituida por una bifurcacin, ya sea abierta o cerrada. 3. Toda sentencia simple puede ser sustituida por una cualquiera de las tres iteraciones presentadas. 4. Las sustituciones indicadas en 1, 2 y 3 pueden realizarse tantas veces como sea necesario, llevando a unas estructuras anidadas dentro de otras. Todo algoritmo define un camino que, desde un estado inicial Hasta 10 Repetir

representado con una circunferencia de inicio, gua a un proceso hasta un estado final, representado con una circunferencia de trmino, en el que, si el algoritmo ha quedado bien definido, nuestro problema habr quedado resuelto. En un algoritmo siempre debe haber un y slo un estado inicial, y un y slo un estado final o de trmino. Un peligro grave en todo algoritmo es quedar atrapado en una estructura de repeticin o de iteracin: llegar a una sentencia iterada gobernada por una condicin que nunca deje de ser verdadera. En ese caso, aunque supuestamente tengamos definida una ruta que nos llevara hasta el estado final, en realidad el proceso definido por el algoritmo nunca saldra de esa iteracin. Suponga, por ejemplo, el algoritmo del clculo del factorial propuesto en la Figura 5.2. Suponga que, al introducir el valor de el usuario le asigna un valor negativo: por ejemplo 5. Tal y como hemos definido el

74

Captulo 5. Algoritmos.

procedimiento, el valor de

se ir decrementando de 1 en 1, pero no

alcanzar en ningn caso el valor cero, por lo que nunca ocurrir que la condicin de la iteracin sea falsa, y se habr cado en una iteracin infinita, que no tiene salida posible. Aunque la experiencia en el uso de estas estructuras ya nos lo ensea, conviene ahora sealar que es necesario que en el Bloque de instrucciones se produzca alguna variacin en alguno de los valores que intervienen en la expresin de control (o vare el valor de la variable de control para el ciclo repetitivo controlado por variable). De lo contrario, si la expresin que condiciona la repeticin del bloque iterado es verdadera en su inicio, entonces nada har que deje de serlo despus de cada nueva iteracin, y el algoritmo quedar atrapado en la ejecucin interminable de ese ciclo de instrucciones. (Todos manual.) los conceptos sobre el paradigma de la programacin

estructurada estn, de nuevo, presentados en el captulo 6 de este

Otros ejemplos de construccin de algoritmos.


Aunque no se ha llegado todava a explicar en este libro cmo se expresan en el lenguaje C las diferentes estructuras bsicas de la programacin estructurada; y aunque slo se ha visto hasta el momento un capitulo introductorio sobre ese lenguaje; incluimos, en estos ejemplos que a continuacin se recogen, el cdigo, en C, que expresa los algoritmos que se proponen como solucin a los enunciados.

5.1.

Proponer un algoritmo que indique cul es el menor de tres enteros recibidos.

75

Informtica Aplicada. Programacin en Lenguaje C.

Algoritmo M: Devuelve el menor de tres enteros recibidos como entrada del proceso. M0 M1 M2 M3 M4 [Entrada] [Inicializar] [Es Menor b ?] [Es Menor c ?] [Mostrar resultado] Valores , y

Mostrar

Pseudocdigo: 1. 2. 3. 4. 5. 6. Leer , y

Si entonces: Si entonces: [Mostrar resultado]: Mostrar: Fin

El cdigo en C de este algoritmo podra ser el que sigue: int main(void) { short int m, a, b, printf(Valor de a printf(Valor de b printf(Valor de c m = a; if(m > b) m = b; if(m > c) m = c; printf(\nEl menor return 0; }

c; ...); scanf(%hd, &a); ...); scanf(%hd, &b); ...); scanf(%hd, &c); // Asignamos a m el valor de a. // Si m es mayor que b ... // Si m es mayor que c ... valor introducido es %hd\n, m);

Leer

C1
S

C1
No

C2
S

C2
No

Mostrar

Figura 5.4.: Flujograma para el algoritmo de bsqueda del menor de tres enteros.

76

Captulo 5. Algoritmos.

Donde las variables variable variables ,

almacenan tres valores numricos

previamente introducidos por el usuario; y donde al final del proceso la contiene el valor menor de los tres contenidos en las y .

Flujograma: (Figura 5.4.)

5.2.

Proponer un algoritmo que indique si un entero recibido es par o es impar

Una solucin sencilla para saber si un entero es par ser dividir el nmero por dos y comprobar si el resto de este cociente es cero. Si el resto es igual a uno, entonces el entero es impar. A la operacin de clculo del resto de una divisin entera se le suele llamar operacin mdulo ( mdulo , Pseudocdigo: 1. 2. 3. 4. 5. Leer [Hallar el resto]: . Si entonces: [Mostrar resultado]: Mostrar: PAR Sino entonces: (es decir, se verifica que [Mostrar resultado]: Mostrar: IMPAR Fin ).

El cdigo en C tiene el siguiente aspecto: int main(void) { short int a , resto; printf(Valor de a ... ); scanf(%hd, &a); resto = a % 2; // operacin clculo de resto. if(resto == 0) printf(PAR);// Si resto = cero a es PAR else printf(IMPAR); // Sino, a es IMPAR return 0; } Flujograma: (Figura 5.5.)

77

Informtica Aplicada. Programacin en Lenguaje C.

Averiguar si un entero es par o impar es una tarea verdaderamente sencilla. Y es evidente que no hay un modo nico o algoritmo para averiguarlo. Veamos otro posible procedimiento o algoritmo para dar respuesta a esta pregunta: 1. 2. 3. 4. Leer Mientras que Repetir: Si entonces: [Mostrar resultado]: Mostrar: PAR Si no entonces: [Mostrar resultado]: Mostrar: IMPAR Fin

Es decir, si el nmero se puede obtener como suma de doses, entonces el entero es par. Si en un momento determinado, a la suma de doses hay que aadirle un uno, entonces el nmero es impar. Como se puede ver, la sentencia 3 es una bifurcacin cerrada. sta queda gobernada por una sola condicin: que que es igual a 1. sea o no igual a 2. La lgica del programa nos indica que si la condicin es falsa, entonces es

El cdigo para este nuevo algoritmo sera el siguiente: int main(void) { short int a; printf(Valor de a ... ); while(a > 2) a -= 2; if(a == 0) printf(PAR); else printf(IMPAR); return 0; }

scanf(%hd, &a);

Leer C
S

No

Figura 5.5.: Flujograma para determinar si un entero es par o impar.

Mostrar PAR
F

Mostrar IMPAR

78

Captulo 5. Algoritmos.

Intente dibujar el diagrama de flujo de este algoritmo Cul de los dos algoritmos es mejor? Pues depende. Si el entero introducido sobre el que se desea averiguar su condicin de par o impar es pequeo, entonces quiz sea mejor este segundo algoritmo, pues unas pocas restas requiere menos trabajo de clculo para un ordenador que una operacin mdulo. Si el entero va siendo grande, entonces posiblemente sea ms rpido emplear el primer procedimiento visto.

5.3.

Proponer un algoritmo que muestre el resultado de una potencia de la que se recibe la base y el exponente

Un modo de calcular la potencia es realizar el producto de la base tantas veces como indique el exponente. Hay que tener la precaucin de que si el exponente es igual a cero, entonces nuestro algoritmo ofrezca como salida el valor uno. El algoritmo, en su forma de pseudocdigo, podra quedar de la siguiente manera: 1. 2. Leer el valor de la base ( ) y del exponente ( ) [Inicializar]:

Leer , C
Si

?
No

Mostrar

Figura 5.6.: Flujograma de un algoritmo para calcular una potencia

79

Informtica Aplicada. Programacin en Lenguaje C.

3. 4. 5.

Mientras repetir 3.1. 3.2. [Mostrar resultado]: Mostrar: Fin

Si vemos el diagrama de flujo de la figura 5.6. comprobamos que en el caso de que la condicin evaluada en el paso 3 sea verdadera (la condicin es : ser verdadera cuando el exponente an no haya alcanzado el valor igual a cero), ocurrir que se ejecutarn una serie de instrucciones y, de nuevo se volver a evaluar la condicin del paso 3. Vemos por tanto que mediante la evaluacin de expresiones se puede determinar que se ejecute una instruccin u otra de nuestro algoritmo; y tambin que se ejecute una instruccin o un bloque de instrucciones o una sola vez, o varias veces, o ninguna vez, dependiendo de que se siga cumpliendo o no la expresin. Con la evaluacin de expresiones podemos, por tanto, controlar el orden de ejecucin de las distintas instrucciones del algoritmo. Y podemos determinar la ejecucin de una u otra instruccin en funcin de una condicin establecida. Y podemos reiterar la ejecucin de una o varias instrucciones tantas veces como permita la condicin de permanencia.

5.4.

Algoritmo de Euclides para el clculo de mximo comn divisor de dos enteros positivos.

Dados dos enteros positivos

y , el algoritmo debe devolver el mayor y a . Euclides demostr una

entero positivo que divide a la vez a

propiedad del mximo comn divisor de dos enteros que permite definir un procedimiento (algoritmo) sencillo para calcular ese valor. Segn la propiedad demostrada por Euclides, se verifica que el mximo comn divisor de dos enteros positivos cualesquiera y ( ) es igual al mximo comn divisor del segundo ( ) y el resto de dividir el primero

80

Captulo 5. Algoritmos.

Leer

C C Figura 5.7.: Flujograma para el algoritmo de Euclides, para el clculo del mximo comn divisor de dos enteros. por el segundo:

Mostrar
F

. A la operacin del clculo

del resto del cociente entre dos enteros, la llamaremos operacin mdulo. Como ver ms adelante, esta operacin est explcitamente definida en el lenguaje C cuando se trabaja con valores enteros. Si tenemos en cuenta que el mximo comn divisor de dos enteros donde el primero es mltiplo del segundo es igual a ese segundo entero, entonces ya tenemos un algoritmo sencillo de definir: Cuando lleguemos a un par de valores ( , ) tales que sea mltiplo de , tendremos que y el mximo comn divisor buscado ser . Pseudocdigo para el algoritmo de Euclides: 1. Leer el valor de los dos enteros y . (Suponemos que ambos son distintos de cero). 2. Mientras que Repetir: 2.1. . 2.2. . 3. [Mostrar resultado]: Mostrar 4. Fin. El diagrama de flujo del algoritmo queda como recoge la Figura 5.7. Y el cdigo en lenguaje C del algoritmo de Euclides tendra el siguiente aspecto (las variables y contienen inicialmente el valor de los ): enteros de los que se desea conocer su mximo comn divisor; ese valor queda finalmente almacenado en la variable

81

Informtica Aplicada. Programacin en Lenguaje C.

int main(void) { short int m, n, r; printf(Valor de m ... ); scanf(%hd, &m); printf(Valor de n ... ); scanf(%hd, &n); while(r = m % n) { m = n; n = r; } printf(El mcd de %hd y %hd es %hd.\n, m, n, r); return 0; }

5.5.

Algoritmo que muestra la tabla de multiplicar de un entero.

Este enunciado no requiere presentacin. Simplemente se trata de un programa que solicita del usuario un valor entero, y entonces el programa muestra por pantalla la clsica tala de multiplicar. Veamos el pseudocdigo: 1. Leer . 2. [Inicializar variable de control]: . 3. [Condicin de permanencia] Mientras que Repetir: 3.1. [Mostrar resultado parcial]: Mostrar ; * ; ; = ; 3.2. [Incrementar variable control]: . 4. Fin Su diagrama de flujo queda recogido en la Figura 5.8.

Leer

? S

No

Mostrar * =

Figura 5.8.: Flujograma para el algoritmo que muestra la tabla de multiplicar de un entero.

82

Captulo 5. Algoritmos.

Tambin podra haberse expresado el pseudocdigo con una iteracin controlada por variable. Quedara de la siguiente forma, muy similar a la anterior: 1. Leer . 2. Para Hasta Repetir: 2.1. [Mostrar resultado parcial]: Mostrar 2.2. [Incrementar variable control]: 3. Fin

; * ; ; = ; .

El cdigo en C de esa segunda forma tiene el aspecto que sigue: int main(void) { short i, N; printf(Valor de i ... ); scanf(%hd, &i); for(i = 0 ; i <= 10 ; i++) printf(%hu * %hu = %hu\n, N, i, N * i); return 0; }

5.6.

Algoritmo que muestra un trmino cualquiera de la serie de Fibonacci.

Fibonacci fue un matemtico italiano del siglo XIII que defini la serie que lleva su nombre. Cada nuevo valor de esta serie es el resultado de la suma de los dos anteriores. Y los dos primeros elementos de la serie son unos. 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... Al margen de propiedades que pudieran facilitar el clculo de un elemento cualquiera de la serie, el algoritmo que, con lo que sabemos de esta serie, resuelve nuestro problema, pudiera tener el siguiente pseudocdigo (el diagrama de flujo del algoritmo queda recogido en la figura 5.9.): 1. Leer el trmino que se desea conocer: . 2. [Inicializar variables]: , , , (los dos

83

Informtica Aplicada. Programacin en Lenguaje C.

primeros elementos de la seria ya los tenemos). 3. [Condicin de permanencia]: Mientras que Repetir: 3.1. . 3.2. . 3.3. . 3.4. [Incrementar variable control]: . 4. [Mostrar resultados]: Mostrar . 5. Fin.

Leer

No Mostrar

Figura 5.9.: Flujograma para el algoritmo que muestra el trmino -simo de la serie de Fibonacci.

O tambin: 1. Leer el trmino que se desea conocer: . 2. [Inicializar variables]: , , 3. [Clculos]: Para Hasta Repetir: 3.1. . 3.2. . 3.3. . 3.4. [Incrementar variable control]: . 4. [Mostrar resultados]: Mostrar . 5. Fin. Podemos ver el cdigo en C de las dos algoritmos propuestos para resolver este enunciado: int main(void) { short int fib1 = 1 , fib2 = 1, Fib, n, i = 2; printf(El valor de n ... , n); scanf(%hd, &n); while(i <= n) {

84

Captulo 5. Algoritmos.

Fib = fib1 + fib2; fib1 = Fib2; fib2 = Fib; i++; } printf(El elemento %hd es %hd.\n, n, Fib); return 0; } De acuerdo con la segunda forma del algoritmo, el cdigo sera: int main(void) { short int fib1 = 1 , fib2 = 1, Fib, n, i; printf(El valor de n ... , n); scanf(%hd, &n); for(i = 2 ; i <= n ; i++) { Fib = fib1 + fib2; fib1 = Fib2; fib2 = Fib; } printf(El elemento %hd es %hd.\n, n, Fib); return 0; }

Recapitulacin.
En este captulo hemos definido el concepto de algoritmo, y hemos presentado dos herramientas para su formulacin: el pseudocdigo y los flujogramas o diagramas de flujo. Hemos distinguido entre sentencias que ejecutan instrucciones y sentencias que definen estructuras de control, que permiten determinar qu instrucciones se deben ejecutar en cada momento y cules no. Hemos empleado estructuras condicionales de decisin y de iteracin. Y hemos mostrado diferentes ejemplos de construccin de algoritmos. Ya hemos dicho que la construccin de algoritmos requiere cierta dosis de oficio y experiencia. No se gan Zamora en una hora. Se proponen a continuacin algunos ejercicios que pueden ayudar a afianzar los conceptos y herramientas recin estudiados.

85

Informtica Aplicada. Programacin en Lenguaje C.

Otros ejercicios propuestos.

5.6.

Disee un algoritmo que resuelva una ecuacin de segundo grado. Tendr como entrada los coeficientes a, y de la ecuacin y ofrecer como resultado las dos soluciones reales.

1. Leer los coeficientes , y . 2. [Si la ecuacin es de grado 1]: Si entonces: 2.1. [Si la ecuacin no existe]: Si entonces: [Mostrar mensaje]: Mostrar: Ecuacin errnea 2.2. [Sino: si la ecuacin s existe y es de grado 1]: Sino entonces: [Mostrar solucin]: Mostrar: . 3. [Sino: Si la ecuacin es de grado 2]: Sino entonces: 3.1. [Calcular Discriminante]: . 3.2. [Si soluciones imaginarias]: Si entonces: [Mensaje]: Mostrar: No hay soluciones reales. 3.3. [Sino: soluciones reales]: Sino entonces: [Mostrar soluciones]: 3.3.1. Mostrar: . 3.3.2. Mostrar: . 4. Fin. Dibuje el flujograma del algoritmo presentado. El cdigo de este algoritmo es el siguiente: #include <stdio.h> #include <math.h> int main (void) { short a, b, c; double x1, x2, d; printf(COEFICIENTES ...\n); printf(Coeficiente a: ); scanf(%hd, &a); printf(Coeficiente b: ); scanf(%hd, &b); printf(Coeficiente c: ); scanf(%hd, &c); if(!a) { if(!b) printf(Ecuacin Errnea.\n); else printf(Solucin: %lf\n, x1 = -c / b);

86

Captulo 5. Algoritmos.

} else { d = (double)b * b 4 * a * c; if(d < 0) printf(Sin soluciones reales.\n); else { printf(x1: %lf\n, x1 = (-b + sqrt(d))/(2*a)); printf(x2: %lf\n, x2 = (-b - sqrt(d))/(2*a)); } } return 0; }

5.7.

Disee un algoritmo que muestre todos los divisores de un entero que se recibe como entrada del algoritmo.

Todos los divisores de un entero

son enteros comprendidos entre la , y el mismo .

unidad y la parte entera de la mitad de

El diagrama de flujo podra ser el recogido en la Figura 5.10. Escriba el pseudocdigo flujograma. correspondiente al algoritmo representado por ese

Leer

Mostrar

C1

C1
No

C2 C2
No

Mostrar

Mostrar
F

Figura 5.10.: Algoritmo para la bsqueda de los divisores de un entero dado.

87

Informtica Aplicada. Programacin en Lenguaje C.

5.8.

Disee un algoritmo que recibe y dibuje el flujograma.

enteros como entrada y

muestra el mayor de ellos en la salida. Escriba el pseudocdigo

Intente resolverlo usted mismo, por su cuenta.

5.9.

Disee un algoritmo que calcule el nmero nmero verifica la siguiente relacin:

sabiendo que este

Escriba el pseudocdigo y dibuje el flujograma.

Intento resolverlo usted mismo, por su cuenta.

5.10.

Haga lo mismo sabiendo que el nmero relacin:

verifica la siguiente

Para solventar estos dos ltimos ejercicios es evidente que no podemos realizar infinitas sumas: por ms rpido que sume el ordenador, para realizar sumas sin fin nunca habr tiempo suficiente, como es obvio. Podemos solventar el problema planteado de una forma bastante simple sin ms que limitando el nmero de operaciones a una cantidad alta pero finita: por ejemplo diez mil, o un milln de veces, por decir algo.

88

Captulo 5. Algoritmos.

El pseudocdigo del segundo de estos dos ltimos enunciados sera algo as como lo que a continuacin se propone: 1. [Inicializar variables]: , , 2. [Clculos]: Mientras que Repetir: 2.1. [Sumatorio] . 2.2. [Incrementar ]: k . 3. [Ajustar y mostrar resultado]: Mostrar 4. Fin. El cdigo en C quedara de la siguiente forma: #include <stdio.h> #include <math.h> int main (void) { double sumatorio = 0; long k = 1; while(k < 1000000) sumatorio += 1 / ((double)k * k++); printf(El nmero PI es %lf.\n, sqrt(6 * sumatorio)); return 0; }

5.11.

Disee un algoritmo que calcule la media de todas los notas introducidas. Convenimos en que el proceso de entrada de datos termina cuando se introduzca una calificacin negativa, sta se considerar no vlida e indicar el final de entrada de datos

1. [Inicializar variables]: 1.1. [contador de calificaciones]: 1.2. [suma de calificaciones]: 2. Mientras que Repetir: 2.1. [Entrada de datos]: Leer . 2.2. [Si la nota no es negativa] Si Entonces: 2.2.1. [Nueva nota vlida]: 2.2.2. [Suma de notas]: . 3. [Mostrar resultados]: 3.1. [Verificar que al menos hay una nota]:

89

Informtica Aplicada. Programacin en Lenguaje C.

Si Entonces Mostrar: . 3.2. Sino entonces: Mostrar: No se han introducido notas. 4. Fin. Este algoritmo quedara mejor si no hubiera que verificar dos veces en cada nota introducida si la calificacin es mayor o igual que cero. El algoritmo podra quedar mejor as (ver flujograma en figura 5.11.): 1. [Inicializar variables]: 1.1. [contador de calificaciones]: 1.2. [suma de calificaciones]: 2. Repetir Siempre: 2.1. [Entrada de datos]: Leer . 2.2. [Si la nota no es negativa] Si Entonces: 2.2.1. [Abandonar la iteracin]. Ir al paso 3. 2.3. [Nueva nota vlida]: 2.4. [Suma de notas]: . 3. [Mostrar resultados]: 3.1. [Verificar que al menos hay una nota]: Si Entonces Mostrar: . 3.2. Sino entonces: Mostrar: No se han introducido notas. 4. Fin.
C

Leer

Figura 5.11.: Algoritmo para mostrar la media de unas notas introducidas hasta terminar con una nota negativa. C1 ? C2 C2
No

C1

No S

Mostrar

Mostrar No hay notas

La sentencia recogida en 2.2.1. no es un salto cualquiera: es la forma que toman las estructuras de iteracin como las mostradas en la Figura 5.3.(c). Aqu hemos introducido una nueva expresin de pseudocdigo: la de Repetir Siempre. Es una forma como la ya vista de Repetir

90

Captulo 5. Algoritmos.

<sentencias>

Hasta

<condicin>,

en

la

que

la

condicin

de

permanencia jams llega a ser falsa, y por tanto jams se podr abandonar el ciclo salvo por el hecho de que dentro del ciclo se ha incluido una sentencia de salto: Ir a paso 3. Esta es la nica circunstancia en la que vamos a permitirnos el uso de saltos en nuestros algoritmos: saltos empleados para abandonar la ejecucin de un bloque de sentencias gobernadas mediante una estructura de control. Existen sentencias en los lenguajes de que permiten realizar estos saltos. El cdigo en C de este algoritmo quedara de la siguiente manera: int main (void) { double nota, suma = 0; short cont = 0; do { printf(nueva nota ... ); if(nota < 0) break; suma += nota; cont++; }while(1); scanf(%lf, &nota);

if(cont != 0) printf(Media: %lf.\n, suma / cont); else printf(No se han introducido notas.\n); return 0; }

91

Informtica Aplicada. Programacin en Lenguaje C.

92

CAPTULO 6
MODELO DE REPRESENTACIN.
En este captulo queremos introducir una serie de conceptos necesarios para la comprensin del proceso de construccin de algoritmos y programas: la abstraccin y la modularidad. Tambin se incide en la nocin de paradigma de programacin, que ya se introdujo en el captulo anterior. El objetivo final del captulo es lograr que se comprenda el modo en que se aborda un problema del que se busca una solucin informtica. Qu informacin del problema es importante, cmo se codifica, cmo se plantea la definicin de tareas que debern ser resueltas mediante diferentes algoritmos.

Introduccin.
La creacin de un programa no es una tarea lineal. Se trata de disear una solucin, un algoritmo o conjunto de algoritmos, capaces de dar una

Informtica Aplicada. Programacin en Lenguaje C.

solucin al problema planteado. No existe habitualmente una nica solucin, y muchas veces tampoco se puede destacar una de ellas como mejor que las dems. Adems, la solucin adoptada debe ser eficiente, que logre hacer buen uso de los recursos de los que se dispone. Uno de esos recursos es el tiempo: no todas las soluciones son igualmente rpidas. Hay muchos problemas para los que an no se ha obtenido una solucin aceptable. Adems, los programas necesitan con frecuencia modificaciones en sus instrucciones o en las definiciones de sus datos. Los problemas evolucionan y sus soluciones tambin. Poco a poco mejora la comprensin de los problemas que se abordan, y por tanto soluciones antes adoptadas necesitan pequeas o no tan pequeas modificaciones. Los algoritmos cambian a medida que se conoce mejor el problema y su entorno, y en la medida en que se aprenden caminos ms eficientes para alcanzar soluciones parciales o globales de nuestro problema. El primer paso cuando se pretende resolver un problema mediante medios informticos consiste en la abstraccin del problema en busca de un modelo que lo represente. As, mediante la creacin de una representacin simplificada, se consideran slo aquellos detalles que nos interesan para poder tratar el problema. Primeramente hay que determinar cul es exactamente el resultado que se busca y se desea obtener; luego cules son los valores de entrada de los que disponemos, identificando aquellos que sean realmente necesarios para la consecucin de nuestro objetivo. Por ltimo, hay que determinar con precisin los pasos que deber seguir el proceso para alcanzar el resultado final buscado. Al conjunto formado por el problema, con todos sus elementos y la solucin buscada, y todo su entorno, es a lo que llamaremos sistema.

94

Captulo 6. Modelo de representacin.

Abstraccin.
La abstraccin es la capacidad de identificar los elementos ms significativos de un sistema que se est estudiando, y las relaciones entre esos elementos. La correcta abstraccin del sistema que se aborda capacita para la construccin de modelos que permiten luego comprender la estructura del sistema estudiado y su comportamiento. Cada elemento que define a nuestro sistema modelizado tendr una representacin en un tipo de dato y con un valor concreto tomado de un rango de valores posibles. Y cada relacin entre elementos pude definirse mediante expresiones o procedimientos. La abstraccin es un paso previo en la construccin de cualquier programa. abstraccin: 1. Por un lado se deben determinar los tipos de datos que interviene en el sistema, es decir, cul es el conjunto de parmetros que definen su estado en todo momento y su rango de valores posibles, y las operaciones que pueden realizarse con esos valores. Tambin interesa determinar cules son los valores iniciales y los resultados finales que resumen los estados inicial y final del sistema. 2. Por otro lado, se debe tambin determinar las funciones o procedimientos del sistema: los procedimientos que definen su comportamiento. Fundamentalmente hablaremos de dos formas de

Modularidad.
La modularidad es la capacidad de dividir el sistema sobre el que estamos trabajando en sus correspondientes partes diferenciadas (mdulos), cada una de ellas con sus propias responsabilidades y subtareas. En una buena modularizacin de un sistema, para cada uno de los mdulos deben quedar bien definidas sus relaciones con todos los dems mdulos, su modo de comunicacin con todo el resto del

95

Informtica Aplicada. Programacin en Lenguaje C.

sistema. Qu sea lo que se considera por mdulo depende del paradigma de programacin que se utilice. En el lenguaje C, que es un lenguaje del paradigma imperativo y estructurado (ya veremos ms adelante en este captulo estos conceptos) a cada mdulo lo llamaremos funcin o procedimiento. En Java, que es un lenguaje de paradigma de programacin orientado a objetos, un mdulo puede ser una clase o cada uno de los mtodos que implementa. La modularidad permite convertir un problema en un conjunto de problemas menores, ms fciles de abordar. As se logra la divisin del trabajo entre programadores o equipos de programadores, se aumenta la claridad del software que se desarrolla y se favorece la reutilizacin de parte del software desarrollado para problemas distintos para los que pudiera haber algn mdulo semejante a los ya desarrollados. Adems, en muchas ocasiones, este modo de trabajar reduce los costes de desarrollo del software y de su posterior mantenimiento. Esta tarea, tan ventajosa, no es en absoluto trivial. Determinar correctamente los mdulos que describen el funcionamiento del sistema y lograr definir las relaciones entre todos ellos puede llegar a ser muy complicado. De hecho esta modularizacin de un sistema se realiza siempre mediante tcnicas de refinamientos sucesivos, pasando de un problema general a diferentes mdulos que, a su vez, pueden considerarse como otro problema a modularizar; y as sucesivamente, hasta llegar a partes o mdulos muy simples y sencillos de implementar. Para mejor comprender este concepto de modularidad proponemos un ejemplo sencillo. Todos conocemos el Tringulo de Tartaglia: est formado por nmeros combinatorios, ordenados de una forma concreta, tal y como se muestra en la figura 6.1., donde la expresin de los nmeros combinatorios tambin es conocida:

96

Captulo 6. Modelo de representacin.

Queremos hacer un programa que muestre una cualquiera de las filas del tringulo de Tartaglia. El programa preguntar al usuario qu fila desea que se muestre, y luego, mediante el algoritmo que definamos, calcular los valores de esa fila, que mostrar luego por pantalla.

fila 1 fila 2 fila 3 fila 4

...
fila k Figura 6.1.: Distribucin de valores en el Tringulo se Tartaglia

El algoritmo que logra resolver nuestro problema tendra la siguiente forma inicial: Algoritmo de Tartaglia: 1. 2. 3. [Entrada del n de la fila a mostrar]: Leer Para Hasta Repetir: 2.1. Fin. .

[Mostrar resultado parcial]: Mostrar binomio:

El flujograma de este algoritmo quedara como se recoge en la figura 6.2., donde se ve que hemos utilizado el diagrama de Llamada a Procedimiento. Esta llamada a procedimiento significa y exige la creacin de un nuevo programa o mdulo que calcule el binomio o nmero combinatorio. Hay, pues, que disear un nuevo algoritmo, que se habr de emplear repetidamente en nuestro primer algoritmo diseado para mostrar una lnea del tringulo de Tartaglia. Este segundo algoritmo podra tener la

97

Informtica Aplicada. Programacin en Lenguaje C.

Leer C C ?

Mostrar

Figura 6.2.: Algoritmo para clculo del Tringulo de Tartaglia. siguiente forma: Algoritmo del Binomio: 1. 2. 4. 3. 4. 5. 6. 7. [Entrada de los valores de [Calcular]: . [Calcular]: [Calcular]: . [Calcular]: [Calcular]: . [Devolver valor]: Devolver Fin. y ]: Leer . . . y

El flujograma de este nuevo algoritmo queda recogido en la Figura 6.3., donde de nuevo nos encontramos con varias llamadas a un mismo procedimiento: la que se emplea para calcular el valor factorial de los diferentes valores. Y es que, como ya se dijo en el captulo anterior, un algoritmo debe estar compuesto por operaciones simples: el ordenador no sabe calcular el valor factorial de un entero y hay, por tanto, que definir tambin un algoritmo que calcule ese valor para un entero dado. (Este algoritmo lo hemos dejado definido en la Figura 5.2., en el captulo anterior.) En el paso 6 de nuestro Algoritmo del Binomio ya no hemos puesto [Mostrar resultado], sino [Devolver valor], puesto que el valor del binomio no es la solucin de nuestro problema: es un clculo intermedio necesario para lograr encontrar nuestra informacin verdaderamente buscada: la fila del Tringulo de Tartaglia indicada como entrada de todo

98

Captulo 6. Modelo de representacin.

Recibir y

Figura 6.3.: Algoritmo para el clculo del Binomio de Newton.

Devolver

el proceso que estamos afrontando. Por lo mismo, el algoritmo del clculo del Factorial presentado en el Captulo anterior deber sufrir una pequea modificacin, pues no se espera que muestre resultado alguno, sino simplemente que devuelva o facilite el valor del factorial del entero recibido, pues de nuevo no hay inters alguno en conocer ningn valor factorial calculado. Con este ejemplo hemos visto que para afrontar y resolver

satisfactoriamente un problema es conveniente descomponerlo en partes menores, independientes y ms sencillas, que hacen el proceso ms inteligible y fcil de resolver. Es a este proceso al que llamamos de creacin de mdulos, o modularizacin. La apariencia del cdigo para la resolucin de este problema, en lenguaje C, es la que sigue (no se preocupe si ahora le parece complicado: realmente no lo es tanto: ya lo aprender): #include <stdio.h> /* Declaracin de mdulos o funciones definidas. --------- */ void Tartaglia(unsigned short); unsigned long Binomio(unsigned short, unsigned short); unsigned long Factorial(unsigned short); /* Funcin Principal. ------------------------------------ */ int main(void)

99

Informtica Aplicada. Programacin en Lenguaje C.

{ unsigned short fila; printf("Aplicacin que muestra una fila); printf( del tringulo de Tartaglia.\n\n"); printf("Indique nmero de la fila a visualizar ... "); scanf("%hu", &fila); Tartaglia(fila); return 0; } void Tartaglia(unsigned short f) { unsigned short i; for(i = 0 ; i < f ; i++) printf("%lu\t", Binomio(f - 1 , i )); } unsigned long Binomio(unsigned short m, unsigned short k) { return Factorial(m) / (Factorial(k)*Factorial(m - k)); } unsigned long Factorial(unsigned short n) { unsigned long Fact = 1; while(n) Fact *= n--; return Fact; } Como puede comprobar en el cdigo, la funcin main (funcin principal) se limita a recibir del usuario del programa el nmero de la fila que quiere visualizar, e invoca a la funcin tartaglia. A su vez, esta funcin simplemente invoca varias veces la funcin binomio: cada vez con un valor de I incrementado en uno, y hasta llegar a su valor mximo, con el valor de I igual a f 1. Y a su vez la funcin binomio invoca por tres veces a la funcin factorial. Otro ejemplo: supongamos que queremos hacer un programa que sirva para operar con nmeros complejos. Queremos definir las operaciones de suma, de resta y de producto de dos complejos. Vamos a representar aqu un nmero complejo como un par de reales: y son reales. donde

100

Captulo 6. Modelo de representacin.

F (c)

(a)

(b)

Figura 6.4.: Operaciones con nmeros complejos: (a) Suma; (b) Resta; (c) Producto. Habr que definir tres mdulos o procedimientos que definan cada una de las tres operaciones. Cada uno de ellos recibe como datos de entrada dos nmeros complejos, o lo que es lo mismo dos pares de nmeros reales; y ofrece como salida un nmero complejo. La definicin de los tres mdulos queda definido por los algoritmos representamos en los flujogramas de la Figura 6.4. El algoritmo de la aplicacin (lo desarrollamos ahora en pseudocdigo) podra tener la siguiente definicin. Suponemos que la aplicacin recibe como entrada, adems de los complejos a operar, un carcter que indica cul es la operacin que se desea realizar. 1. [Entrada de datos]: , , . 2. Si , Entonces . 3. Sino Entonces: 3.1. Si Entonces 3.2. Sino Entonces 3.2.1. Si Entonces 3.2.2. Sino Entonces [Mostrar Mensaje]: Mostrar: op. invlido. 4. Si Entonces

101

Informtica Aplicada. Programacin en Lenguaje C.

[Mostrar Resultado]: Mostrar 5. Fin.

Evidentemente, si el usuario solicita una operacin no definida, el algoritmo deber advertirlo y no realizar nada. Como ya hemos vislumbrado en ambos ejemplos, los mdulos que se obtienen deben gozar de algunas propiedades, necesarias si se desea que la modularizacin resulte finalmente til: 1. Independencia funcional. Es decir, cada mdulo definido debe realizar una funcin concreta o un conjunto de funciones afines (alta cohesin), sin apenas ninguna relacin con el resto (bajo acoplamiento). En el ejemplo de las operaciones con valores complejos, tenemos definidos tres mdulos que en nada interfieren el uno con el otro. En el ejemplo del tringulo de Tartaglia tenemos dos niveles de independencia: el mdulo Binomio necesita del mdulo Factorial. Para que el mdulo Factorial se ejecute correctamente no necesita de nada del resto del proceso definido excepto, claro est, de una valor de entrada. Y para que el mdulo Binomio ejecute correctamente no necesita de nada del resto del proceso definido en su mismo nivel excepto, de nuevo, de dos valores de entrada. 2. Comprensibilidad. Es decir, cada mdulo debe ser comprensible de forma aislada. Para lograr eso, desde luego se requiere la independencia funcional; y tambin establecer correctamente las relaciones entre cada mdulo definido y los restantes. En nuestros dos ejemplos, cada uno de los mdulos realiza una tarea concreta y bien definida. En el ejemplo del Tringulo de Tartaglia, se han implementado tres funciones tiles, cada una de las cuales realiza una operacin concreta y conocida: una construye la lnea solicitada del tringulo; otra calcula el valor del binomio de Newton; y otra devuelve el valor del factorial de un entero recibido. All

102

Captulo 6. Modelo de representacin.

donde se recoja ese cdigo, se podrn utilizar estas funciones 3. Adaptabilidad. Es decir, los mdulos se deben definir de manera que permitan posteriores modificaciones o extensiones, realizadas tanto en ese mdulo como en los mdulos con los que se relaciona. En nuestro ejemplo del tringulo de Tartaglia, el mdulo Factorial, una vez definido, ya es vlido para todo proceso que necesite el clculo de ese valor. De hecho ni siquiera ha sido necesario presentar este mdulo en este captulo porque ha bastado hacer referencia a la descripcin que ya se hizo en el captulo anterior: en nada importa el entorno concreto en el que queremos que se ejecute el algoritmo de clculo del factorial. 4. Una advertencia importante para la correcta definicin y construccin de un nuevo mdulo: la correcta descripcin del modo en que se utiliza: lo que podramos llamar como una correcta definicin de su interfaz. Las tres exigen propiedades que cuando previas se de la un modularidad mdulo o presentadas, defina

procedimiento quede perfectamente determinado el modo en que se har uso de l: qu valores y en qu orden y forma espera recibir ese mdulo como entrada para realizar correctamente el proceso descrito con su algoritmo. Y qu valores ofrece como resultado (y de nuevo en qu orden y forma) el mdulo al finalizar las sentencias del algoritmo. Para que un mdulo se considere debidamente definido debe ocurrir que, para cualquiera que desee usarlos, le sea suficiente conocer nicamente cul es su interfaz. Cul sea la definicin del mdulo, sus sentencias e instrucciones que ejecuta, y su orden, es cuestin que en nada deben importar al usuario de ese mdulo. All donde se desee realizar una operacin suma de complejos se podr acudir al mdulo definido con el algoritmo recogido en la Figura 6.4. (a). Pero siempre que se haga uso de ese mdulo ser necesario que quien lo utilice facilite como entrada los dos pares de reales que definen los dos complejos, operandos de la operacin

103

Informtica Aplicada. Programacin en Lenguaje C.

suma que se desea realizar. El proceso para la construccin de un programa pasa, por tanto, por estos pasos: abstraccin modularizacin diseo de algoritmos implementacin. Entendemos por implementacin el proceso de escribir, en un lenguaje de programacin concreto, cada uno de los algoritmos diseados.

La abstraccin de la informacin: los datos.


En este proceso de abstraccin de la realidad que hemos presentado, se ha seleccionado la informacin necesaria para resolver el problema planteado. Esta informacin viene recogida mediante unas entidades que llamamos datos. Entendemos por dato cualquier objeto manipulable por el ordenador: un carcter ledo por el teclado, una informacin numrica almacenada en el disco de ordenador o que llega a travs de la red, etc. Estos datos pueden estar vinculados entre s mediante un conjunto de relaciones. Un dato puede ser tanto una constante definida dentro del programa y que no altera su valor durante su ejecucin; o dato variable, que puede cambiar su valor a lo largo de la ejecucin el programa. El conjunto de valores posibles que puede tomar un dato variable se llama rango o dominio. Por ejemplo, podemos definir un rango que sea todos los enteros comprendidos entre 0 y (256 valores posibles). Los diferentes valores recogidos en un determinado rango se denominan literales. Un literal es la forma en que se codifica cada uno de los valores posibles de un rango o dominio determinado. Entendemos por literal cualquier smbolo que representa un valor. Por ejemplo, 3 representa el nmero 3 (literal numrico), y Este es un texto de siete palabras representa un texto (literal alfanumrico). Los datos debern ser codificados y ubicados en el programa mediante la reserva, para cada una de ellos, de un espacio de memoria. Los

104

Captulo 6. Modelo de representacin.

diferentes valores que pueden tomar esos datos variables quedarn codificados en los distintos estados que puede tomar esa memoria. El estado fsico concreto espacio que de tome en un momento un concreto valor u un otro determinado memoria significar

dependiendo de cul sea el cdigo empleado. Cada espacio de memoria reservado para codificar y ubicar un dato variable (lo llamaremos simplemente variable) deber ser identificado de forma inequvoca mediante un nombre. Para la generacin de esos nombres har falta echar mano de un alfabeto y de unas reglas de construccin (cada lenguaje tiene sus propias reglas). A estos nombres los llamamos identificadores. Los identificadores son smbolos empleados para representar objetos. Cada lenguaje debe tener definidas sus reglas de creacin: en el captulo 4, en el epgrafe Elementos lxicos, estn recogidas las reglas de creacin de identificadores en el lenguaje C. Si un identificador representa un literal, es decir, si se define para un valor concreto, no variable, entonces el identificador representa un dato constante. Por ejemplo, se podra llamar PI a la constante que guarde el literal 3.14159.

Tipo de dato.
Un tipo de dato define de forma explcita un conjunto de valores, denominado dominio (ya lo hemos definido antes), sobre el cual se pueden realizar un conjunto de operaciones. Cada espacio de memoria reservado en un programa para almacenar un determinado tipo de valores debe estar asociado a un tipo de dato. La principal motivacin es la organizacin de nuestras ideas sobre los objetos que manipulamos. Un lenguaje de programacin proporciona un conjunto de tipos de datos simples o predefinidos (que se llaman los tipos de dato primitivos) y adems proporciona mecanismos para definir nuevos tipos de datos,

105

Informtica Aplicada. Programacin en Lenguaje C.

llamados compuestos, combinando los anteriores. Distintos valores pertenecientes a diferentes tipos de datos pueden tener la misma representacin en la memoria. Por ejemplo, un byte con el estado 01000001 codificar el valor numrico 65 si este byte est empleado para almacenar valores de un tipo de dato entero; y codificar la letra A si el tipo de dato es el de los caracteres y el cdigo empleado es el ASCII. Por eso, es muy importante, al reservar un espacio de memoria para almacenar valores concretos, indicar el tipo de dato para el que se ha reservado ese espacio.

Variable.
Una variable es un elemento o espacio de la memoria que sirve de almacenamiento de un valor, referenciada por un nombre, y perteneciente a un tipo de dato. Podemos definir una variable como la cudrupla V = <N, T, R, K> (6.1.)

Donde N es el nombre de la variable (su identificador); T el tipo de dato para el que se cre esa variable (que le indica el dominio o rango de valores posibles); R es la referencia en memoria, es decir, la posicin o direccin de la memoria reservada para esa variable (su ubicacin); y K es el valor concreto que adopta esa variable en cada momento y que vendr codificado mediante un estado fsico de la memoria. Por ejemplo, mediante <x, entero, , 7> nos referimos a una variable (ya ver en su

que se ha llamado x, creada para reservar datos de tipo entero, que est posicionada en la direccin de memoria momento qu forma tienen esas referencias de memoria) y que en este preciso instante tiene codificado el valor entero 7. Al hacer la asignacin

106

Captulo 6. Modelo de representacin.

x 3, se altera el estado de esa memoria, de forma que pase a codificar el valor entero 3. En la mayora de los lenguajes (y tambin en C) es preciso que toda variable se declare antes de su utilizacin. En la declaracin se realiza la asociacin o vinculacin entre el tipo de dato (que explicita el dominio de valores vlidos) y el nombre que se le da a la variable. En algunos lenguajes es obligado, al declarar una variable, indicar su valor inicial; otros lenguajes asignan un valor por defecto en caso de que el programador no asigne ninguno; el lenguaje C no obliga a realizar esta operacin de inicializacin de valores en las variables declaradas: si no se le asigna un valor inicial, esas variables podrn tener cualquier valor de los definidos en el dominio (tipo de dato) en el que han sido declaradas. Ya ver como un error clsico de quien comienza a aprender a programar es descuidar esa operacin de dar valor inicial a las variables. En la declaracin de la variable tambin queda definida la asociacin entre el nombre y el espacio de memoria reservado: antes de la ejecucin de un programa el ordenador ya conoce los requerimientos de espacio que ese programa lleva consigo: cantidad de bytes reservados y posicin de la memoria del ordenador donde se ubica esa variable.

Variable - Tipo de dato Valor.


Una definicin, quiz un poco misteriosa, del concepto valor es: elemento perteneciente a un conjunto. De qu conjunto estamos hablando?: del que queda explicitado mediante la declaracin del tipo de dato. A este conjunto es al que hemos llamado rango o dominio. Una vez introducidos estos tres conceptos (variable, tipo de dato, valor), es til pensar en ellos conjuntamente. Una variable ocupa un porcin de memoria. Que esa porcin sea ms o menos extensa depender de para qu haya sido reservada esa

107

Informtica Aplicada. Programacin en Lenguaje C.

variable. Y eso lo determina el tipo de dato para el que se ha creado esa variable. Por ejemplo, si se crea una variable para almacenar enteros que pueden llegar a tomar valores muy grandes, entonces se necesitar al menos 32 bits para codificar esos valores. Si los enteros son pequeos, quiz baste con 16 bits. Qu valor codifica una variable en un momento determinado tambin depende del tipo de dato. Dos estados iguales pueden codificar valores distintos si se trata de espacios de memoria que codifican tipos de dato diferentes. Y qu valores puede codificar una determinada variable depender, de nuevo, del tipo de dato en el que se haya definido esa variable.

Paradigmas de programacin. Programacin estructurada.


Un lenguaje de programacin refleja cierto paradigma de programacin. Un paradigma de programacin es una coleccin de conceptos que guan el proceso de construccin de un programa y que determinan su estructura. Dichos conceptos controlan la forma en que se piensan y formulan los programas. Existen distintas formas de abordar un problema y darle solucin mediante un programa informtico; distintas enfoques que se pueden adoptar para resolver un problema de programacin. A cada enfoque, a cada forma de actuar, se le llama paradigma. La clasificacin ms comn distingue tres tipos de paradigmas: imperativo, declarativo y orientado a objetos (que es una extensin del imperativo). Vamos a ceirnos aqu a los lenguajes imperativos, que es el paradigma del lenguaje C. Las caractersticas ms importantes de los lenguajes imperativos son: (1) el concepto de variable como medio para representar el estado del

108

Captulo 6. Modelo de representacin.

proceso computacional, y la instruccin de asignacin como medio para cambiar el valor de una variable, (2) las acciones y funciones como medio de descomposicin de los programas, y (3) las instrucciones de control que permiten establecer el orden en que se ejecutan las instrucciones. Dentro de estos lenguajes imperativos cabe distinguir a su vez los estructurados y los no estructurados. Los lenguajes estructurados incluyen estructuras de control que permiten determinar el orden de ejecucin de las sentencias del algoritmo. La programacin estructurada establece las siguientes reglas: 1. Todo programa consiste en una serie de acciones o sentencias que se ejecutan en secuencia, una detrs de otra. Si tomamos el ejemplo del captulo anterior del clculo del trmino de la serie de Fibonacci, podemos esquematizar el proceso en estos pasos: Entrada Inicializar Variables Clculo trmino Mostrar Valor

2. Cualquier accin puede ser sustituida por dos o ms acciones en secuencia. Esta regla se conoce como la de apilamiento. Por ejemplo, el paso antes visto: Inicializar Variables puede representarse mejor como:

109

Informtica Aplicada. Programacin en Lenguaje C.

3. Cualquier accin puede ser sustituida por cualquier estructura de control; y slo se consideran tres estructuras de control: la secuencia (ejecucin secuencias de sentencias, una detrs de otra), la seleccin (o decisin) y la repeticin. Esta regla se conoce como regla de anidamiento. Todas las estructuras de control de la programacin estructurada tienen un solo punto de entrada y un solo punto de salida. (Estas estructuras de control ya las hemos visto en el captulo 5 del manual.) Por ejemplo, la sentencia: Clculo trmino puede representarse mejor como:

No

S Las reglas de apilamiento y de anidamiento pueden aplicarse tantas veces como se desee y en cualquier orden. El lenguaje C es un lenguaje imperativo y estructurado.

Recapitulacin.
En este captulo hemos presentado una serie de conceptos muy necesarios para comprender el trabajo de la programacin. La abstraccin y la modularidad, como pasos previos e imprescindibles en todo trabajo de construccin de una aplicacin: muchas horas de trabajo y de pensar y de hablar, con un bloque de papeles en la mesa, sin teclados ni pantallas, previas a la seleccin o definicin de algoritmos, y previas a la implementacin a uno u otro lenguaje de programacin.

110

Captulo 6. Modelo de representacin.

Y hemos presentado el concepto de paradigma de programacin, que determina distintos modos de trabajar y de formas de plantear soluciones. Los lenguajes de programacin quedan clasificados en funcin de su paradigma. Para cada modo de plantear y resolver un problema existe un conjunto de lenguajes aptos para expresar el modelo abstrado y modularizado. Ejemplo de lenguaje de paradigma imperativo y estructurado es el lenguaje C. El hecho de que el lenguaje C pertenezca a este paradigma implica un modo de trabajar y de programar. Ya ha quedado dicho en este captulo y en el anterior, pero no viene mal repetirlo: en lenguaje C no se deben emplear sentencias de salto: todo el control del flujo del programa debe organizarse mediante estructuras de control. Ejemplos de lenguaje para programacin orientada a objetos son los conocidos C++ y Java, entre otros. Aprender Java, por ejemplo, no es una tarea que se limite a conocer las reglas sintcticas de este lenguaje: requiere conocer a fondo el planteamiento que lleva consigo el paradigma de la programacin orientada a objetos. Pretender programar en Java sin conocer su paradigma es como intentar que un mono hable.

111

Informtica Aplicada. Programacin en Lenguaje C.

112

CAPTULO 7
TIPOS DE DATO Y VARIABLES EN C.
Un tipo de dato define de forma explcita un conjunto de valores, denominado dominio, sobre el cual se pueden realizar una serie de operaciones. Un valor es un elemento del conjunto que hemos llamado dominio. Una variable es un espacio de la memoria destinada al almacenamiento de un valor de un tipo de dato concreto, referenciada por un nombre. Son conceptos sencillos, pero muy necesarios para saber exactamente qu se hace cuando se crea una variable en un programa. Un tipo de dato puede ser tan complejo como se quiera. Puede necesitar un byte para almacenar cualquier valor de su dominio, o requerir de muchos bytes. Cada lenguaje ofrece una coleccin de tipos de datos, que hemos llamado primitivos. Tambin ofrece herramientas para crear tipos de dato distintos, ms complejos que los primitivos y ms acordes con el tipo de problema que se aborde en cada momento.

Informtica Aplicada. Programacin en Lenguaje C.

En este captulo vamos a presentar los diferentes tipos de datos primitivos que ofrece el lenguaje C. Veremos cmo se crean (declaran) las variables, qu operaciones se pueden realizar con cada una de ellas, y de qu manera se pueden relacionar unas variables con otras para formar expresiones. Veremos las limitaciones en el uso de las variables segn su tipo de dato. Ya hemos dicho que un tipo de dato especifica un dominio sobre el que una variable de ese tipo puede tomar sus valores; y unos operadores. A lo largo del captulo iremos presentando los distintos operadores bsicos asociados con los tipos de dato primitivos del lenguaje C. Es importante entender la operacin que realiza cada operador y sobre qu dominio este operador est definido.

Declaracin de variables.
Antes de ver los tipos de dato primitivos, conviene saber cmo se crea una variable en C. Toda variable debe ser declarada previa a su uso. Declarar una variable es indicar al programa un identificador o nombre para esa variable, y el tipo de dato para la que se crea esa variable. La declaracin de variable tiene la siguiente sintaxis: tipo var_1 [=valor1, var_2 = valor_2, , var_N = valor_N]; Donde tipo es el nombre del tipo de variable que se desea crear, y var_1, es el nombre o identificador de esa variable. Aclaracin a la notacin: en las reglas sintcticas de un lenguaje de programacin, es habitual colocar entre corchetes ([]) aquellas partes de la sintaxis que son optativas. En este caso tenemos que en una declaracin de variables se pueden declarar una o ms variables del mismo tipo, todas ellas separadas por el operador coma. Al final de la sentencia de declaracin de variables

114

Captulo 7. Tipos de dato y variables en C.

se encuentra, como siempre ocurrir en cualquier sentencia, el operador punto y coma. En la declaracin de una variable, es posible asignarle un valor de inicio. De lo contrario, la variable creada adquirir un valor cualquiera entre todos los explicitados por el rango del tipo de dato, desconocido para el programador. Qu ocurre si una variable no es inicializada? En ese caso, al declararla se dar orden de reservar un espacio de memoria (del tamao que exija el tipo de dato indicado para la variable) para el almacenamiento de los valores que pueda ir tomando esa variable creada. Esa porcin de memoria es un elemento fsico y, como tal, deber tener un estado fsico. Cada uno de los bits de esta porcin de memoria estar en el estado que se ha llamado 1, o en el estado que se ha llamado 0. Y un estado de memoria codifica una informacin concreta: la que corresponda al tipo de dato para el que est reservada esa memoria. Es conveniente remarcar esta idea. No es necesario, y tampoco lo exige la sintaxis de C, dar valor inicial a una variable en el momento de su declaracin. La casustica es siempre enorme, y se dan casos y circunstancias en las que realmente no sea conveniente asignar a la variable un valor inicial. Pero habitualmente es muy recomendable inicializar las variables. Otros lenguajes lo hacen por defecto en el momento de la declaracin de variables; C no lo hace. Otros lenguajes detectan como error de compilacin (errar sintctico) el uso de una variable no inicializada; C acepta esta posibilidad. A partir del momento en que se ha declarado esa variable, puede ya hacerse uso de ella. Tras la declaracin ha quedado reservado un espacio de memoria para almacenar la informacin de esa variable. Si declaramos tipo variable = valor; tendremos la variable <variable, tipo, R, valor>, de la que desconocemos su direccin de memoria. Cada vez que el programa trabaje con variable estar haciendo referencia a

115

Informtica Aplicada. Programacin en Lenguaje C.

esta posicin de memoria R. Y estar refirindose a uno o ms bytes, en funcin del tamao del tipo de dato para el que se ha creado la variable.

Tipos de datos primitivos en C: sus dominios.


Los tipos de dato primitivos en C quedan recogidos en la tabla 7.1. Las variables de tipo de dato carcter ocupan 1 byte. Aunque estn creadas para almacenar caracteres mediante una codificacin como la ASCII (que asigna a cada carcter un valor numrico codificado con esos 8 bits), tambin pueden usarse como variables numricas. En ese caso, el rango de valores es el recogido en la tabla 7.1. En el caso de que se traten de variables con signo, entonces el rango va desde 27 hasta

27 1 .

TIPOS

SIGNO

RANGO DE VALORES MENOR MAYOR

tipos de dato CARCTER: char signed unsigned char char -128 0 +127 +255

tipos de dato ENTERO: int signed unsigned signed unsigned short short long long -32.768 0 -2.147.483.648 0 +32.767 +65.535 +2.147.483.647 4.294.967.295

tipos de dato CON COMA FLOTANTE float double long double -3.402923E+38 -1.7976931E+308 -1.2E+4932 +3.402923E+38 +1.7976931E+308 +1.2E+4932

Tabla 7.1.: Tipos de datos primitivos en C.

116

Captulo 7. Tipos de dato y variables en C.

Para crear una variable de tipo carcter en C, utilizaremos la palabra clave char. Si la variable es con signo, entonces su tipo ser signed char, y si no debe almacenar signo, entonces ser unsigned char. Por defecto, si no se especifica si la variable es con o sin signo, el lenguaje C considera que se ha tomado la variable con signo, de forma que decir char es lo mismo que decir signed char. Lo habitual ser utilizar variables tipo char para el manejo de caracteres. Los caracteres simples del alfabeto latino se representan mediante este tipo de dato. El dominio de las variables char es un conjunto finito ordenado de caracteres, para el que se ha definido una correspondencia que asigna, a cada carcter del dominio, un cdigo binario diferente de acuerdo con alguna normalizacin. El cdigo ms extendido es el cdigo ASCII (American Standard Code for Information Interchange). Las variables tipo entero, en C, se llaman int. Dependiendo de que esas variables sean de dos bytes o de cuatro bytes las llamaremos de tipo short int (16 bits) de tipo long int (32 bits). Y para cada una de ellas, se pueden crear con signo o sin signo: signed short int y signed long int, unsigned short int y unsigned long int. De nuevo, si no se especifica nada, C considera que la variable entera creada es con signo, de forma que la palabra signed vuelve a ser opcional. En general, se recomienda el uso de la palabra signed. Utilizar esa palabra al declarar enteros con signo facilita la compresin del cdigo. Pero queda claro que no es necesario, en ningn caso, explicitarla en la declaracin de una variable. El tamao de la variable int depende del concepto, no introducido hasta el momento, de longitud de la palabra. Habitualmente esta longitud se toma mltiplo de 8, que es el nmero de bits del byte. De hecho la longitud de la palabra viene definido por el mximo nmero de bits que puede manejar el procesador, de una sola vez, cuando hace clculos con enteros.

117

Informtica Aplicada. Programacin en Lenguaje C.

Si se declara una variable en un PC como de tipo int (sin determinar si es short o long), el compilador de C considerar que esa variable es de la longitud de la palabra: de 16 de 32 bits. Es importante conocer ese dato (que depende del compilador), o a cambio es mejor especificar siempre en el programa si se desea una variable corta o larga, y no dejar esa decisin al tamao de la palabra. Excepto en aquellos casos en que sea completamente indiferente cul sea el tamao de la variable entera declarada, se recomienda indicar siempre ese tamao en el cdigo del programa, al declarar esa variable. Una variable declarada como de tipo long se entiende que es long int. Y una variable declarada como de tipo short, se entiende que es short int. Muchas veces se toma como tipo de dato nicamente el modificador de tamao, omitiendo la palabra clave int. Los restantes tipos de dato se definen para codificar valores reales. Hay que tener en cuenta que el conjunto de los reales es no numerable (entre dos reales siempre hay un real y, por tanto, hay infinitos reales). Los tipos de dato que los codifican ofrecen una codificacin finita s numerable. Esos tipos de dato codifican subconjuntos del conjunto de los reales; subconjuntos que, en ningn caso, pueden tomarse como un intervalo del conjunto de los reales. A esta codificacin de los reales se le llama de coma flotante. As codifica el lenguaje C (y muchos lenguajes) los valores no enteros. Tomando como notacin para escribir esos nmeros la llamada notacin cientfica (signo, mantisa, base, exponente: por ejemplo, el numero de Avogadro, 6, 023 1023 : signo positivo, mantisa 6,023, base decimal y exponente 23), almacena en memoria, de forma normalizada (norma IEEE754) el signo del nmero, su mantisa y su exponente. No es necesario, en la codificacin de los valores de coma flotante en el ordenador, codificar cul es la base, porque se entiende que, en todos los casos, se trabaja con la base binaria.

118

Captulo 7. Tipos de dato y variables en C.

Los tipos de dato primitivos en coma flotante que ofrece el lenguaje C son tres: float, que reserva 4 bytes para su codificacin y que toma valores en el rango sealado en la tabla 7.1.; double, que reserva 8 bytes; y long double, que reserva 10 bytes. Desde luego, en los tres tipos de dato el dominio abarca tantos valores positivos como negativos. Existe adems un tipo de dato que no reserva espacio en memoria: su tamao es nulo. Es el tipo de dato void. No se pueden declarar variables de ese tipo. Ms adelante se ver la necesidad y utilidad de tener definido un tipo de dato de estas caractersticas. Por ejemplo es muy conveniente para el uso de funciones. En C el carcter que indica el fin de la parte entera y el comienzo de la parte decimal se escribe mediante el carcter punto. La sintaxis no acepta interpretaciones de semejanza, y para el compilador el carcter coma es un operador que nada tiene que ver con el punto decimal. Una equivocacin en ese carcter causar habitualmente un error en tiempo de compilacin. Como ha podido comprobar, el lenguaje C dedica nueve palabras para la identificacin de los tipos de dato primitivos: void, char, int, float, double, short, long, signed e unsigned. Ya se ha visto, por tanto ms de un 25 % del lxico total del lenguaje C. Existen ms palabras clave para la declaracin y creacin de variables, pero no introducen nuevos tipos de dato primitivos, sino que modifican el modo en que esas variables reservan sus espacios de memoria. Se vern ms adelante.

Formacin de literales
Cuando en una expresin deseamos introducir un valor literal (por ejemplo x = y + 2: aqu 2 es una valor literal) este valor puede interpretarse inicialmente de muchas formas: es entero o es de coma flotante? Viene expresado en base decimal, en base octal o en base

119

Informtica Aplicada. Programacin en Lenguaje C.

hexadecimal? Lo consideramos entero de 2 de 4 bytes; o de coma flotante de 4, de 8, de 10 bytes? Para especificar sin equvoco un valor literal conviene conocer las siguientes reglas: VALORES ENTEROS. Si queremos expresar el valor entero en base octal, deberemos comenzar su escritura con el dgito 0. Si queremos expresar el valor entero en base hexadecimal,

deberemos comenzar su escritura con los caracteres 0x ( 0X, poniendo la letra X que sigue al cero en mayscula) Si queremos expresar el valor entero en base decimal, lo

expresamos como es usual, pero no iniciando nunca ese valor con el dgito 0. Si queremos que el valor expresado se interprete como unsigned, entonces podemos terminar el literal con el sufijo u, U. Si queremos que el valor expresado se interprete como long, entonces podemos terminar el literal con el sufijo l L. Si queremos que el valor expresado se interprete como unsigned long, entonces podemos terminar el literal con los dos sufijos: u U, y l L. Algunos ejemplos: short a = 071; // Expresado en base octal. short b = 0XABCD; // Expresado en base hexadecimal. long c = 37l; // Expresado en base decimal, indicando // que el literal es de tipo long. long d = 081; // Error: va expresado en base octal. // No puede tener el dgito 8. Observaciones: Cuando se expresa el literal en base decimal, y deseamos que ese valor sea negativo, simplemente utilizamos el operador signo (). Al expresar el literal en base hexadecimal o en base octal NO puede usarse ese operador de signo, porque en la misma

120

Captulo 7. Tipos de dato y variables en C.

expresin del literal se indica el signo: bit ms significativo igual a 1: valor negativo; bit ms significativo igual a 0: valor positivo. Algunos ejemplos: short a = 0x90E0; long b = 0x90E0; unsigned short c = 0x90E0; long d = 0xABCD0000u; // // // // // < 0: el + > 0: > 0: < 0: 16 bits: significativo es 1. b vale 0x000090E0. es variable unsigned. comienza con un 1.

En este ltimo ejemplo, al asignar a la variable d un valor que comienza con el dgito 1, y siendo d una variable con signo, la nica interpretacin posible es que su valor sea negativo: el literal indicado ser unsigned realmente, pero el valor recogido no pertenece al dominio de los valores del tipo de dato signed long. VALORES REALES DE COMA FLOTANTE. Cuando deseamos expresar un valor literal de coma flotante utilizamos el carcter punto (.) para indicar la separacin entre la parte entera del valor literal y su parte fraccionaria. Y disponemos entonces de dos posibles sufijos para los tipos de dato de coma flotante: Si deseamos que el valor literal sea expresin de un float, lo indicamos con el sufijo f F; si deseamos que el valor sea expresin de un long double, lo indicamos con el sufijo l L. Si no indicamos nada, se entiende que el valor es double.

Tipos de datos primitivos en C: sus operadores.


Ya se dijo que un tipo de dato explicita un conjunto de valores, llamado dominio, sobre el que son aplicables una serie de operadores. No queda, por tanto, del todo definido un tipo de dato presentando slo su dominio. Falta indicar cules son los operadores que estn definidos para cada tipo de dato. Los operadores pueden aplicarse a una sola variable, a dos variables, e incluso a varias variables. Llamamos operacin unaria a la que se

121

Informtica Aplicada. Programacin en Lenguaje C.

aplica a una sola variable. Una operacin unaria es, por ejemplo, el signo del valor. No tratamos ahora las operaciones que se pueden aplicar sobre una variable creada para almacenar caracteres. Ms adelante hay un captulo entero dedicado a este tipo de dato char. Las variables enteras, y las char cuando se emplean como variables enteras de pequeo rango, adems del operador unario del signo, tienen definidos el operador asignacin, los operadores aritmticos, los relacionales y lgicos y los operadores a nivel de bit. Los operadores de las variables con coma flotante son el operador asignacin, todos los aritmticos (excepto el operador mdulo o clculo del resto de una divisin), y los operadores relacionales y lgicos. Las variables float, double y long double no aceptan el uso de operadores a nivel de bit.

Operador asignacin.
El operador asignacin permite al programador modificar los valores de las variables y alterar, por tanto, el estado de la memoria del ordenador. El carcter que representa al operador asignacin es el carcter =. La forma general de este operador es nombre_variable = expresin; Donde expresin puede ser un literal, otra variable, o una combinacin de variables, literales y operadores y funciones. Podemos definirlo como una secuencia de operandos y operadores que unidos segn ciertas reglas producen un resultado. Este carcter =, en C, no significa igualdad en el sentido matemtico al que estamos acostumbrados, sino asignacin. No debera llevar a equvocos expresiones como la siguiente:

122

Captulo 7. Tipos de dato y variables en C.

a = a + 1; Ante esta instruccin, el procesador toma el valor de la variable a, lo copia en un registro de la ALU donde se incrementa en una unidad, y almacena (asigna) el valor resultante en la variable a, modificando por ello el valor anterior de esa posicin de memoria. La expresin arriba recogida no es una igualdad de las matemticas, sino una orden para incrementar en uno el valor almacenado en la posicin de memoria reservada por la variable a. El operador asignacin tiene dos extremos: el izquierdo (que toma el nombre Lvalue en muchos manuales) y el derecho (Rvalue). La apariencia del operador es, entonces: LValue = RValue; Donde Lvalue slo puede ser el nombre de una variable, y nunca una expresin, ni un literal. Expresiones como a + b = c; 3 = variable; son errneas, pues ni se puede cambiar el valor del literal 3 que, adems, no est en memoria porque es un valor literal; ni se puede almacenar un valor en la expresin a + b, porque los valores se almacenan en variables, y a + b no es variable alguna, sino una expresin que recoge la suma de dos valores codificados en dos variables. Un error de este estilo interrumpe la compilacin del programa. El compilador dar un mensaje de error en el que har referencia a que el Lvalue de la asignacin no es correcto. Cuando se trabaja con variables enteras, al asignar a una variable un valor mediante un literal (por ejemplo, v = 3;) se entiende que ese dato viene expresado en base 10. Pero en C es posible asignar valores en la base hexadecimal. Si se quiere dar a una variable un valor en hexadecimal, entonces ese valor va precedido de un cero y una letra equis. Por ejemplo, si se escribe v = 0x20, se est asignando a la variable v el valor 20 en hexadecimal, es decir, el 32 en decimal, es decir, el valor 100000 en binario.

123

Informtica Aplicada. Programacin en Lenguaje C.

Operadores aritmticos.
Los operadores aritmticos son: 1. Suma. El identificador de este operador es el carcter +. Este operador es aplicable sobre cualquier variable primitiva de C. Si el operador + se emplea como operador unario, entonces es el operador de signo positivo. 2. Resta. El identificador de este operador es el carcter . Este operador es aplicable sobre cualquier variable primitiva de C. Si el operador se emplea como operador unario, entonces es el operador de signo negativo. 3. Producto. El identificador de este operador es el carcter *. Este operador es aplicable sobre cualquier par de variables de tipo de dato primitivo de C: es un operador binario. Existe el operador * como operador unario, pero lo veremos ms adelante, en el captulo dedicado a los punteros. 4. Cociente o Divisin. El identificador de este operador es el carcter /. Este operador es aplicable sobre cualquier variable primitiva de C. Cuando el cociente se realiza con variables enteras el resultado ser tambin un entero, y trunca el resultado al mayor entero menor que el cociente. Por ejemplo, 5 dividido entre 2 es igual a 2. Y 3 dividido entre 4 es igual a 0. Es importante tener esto en cuenta cuando se trabaja con enteros. Supongamos la expresin superficie = (1 / 2) * base * altura; para el clculo de la superficie de un tringulo, y supongamos que todas las variables que intervienen han sido declaradas enteras. As expresada la sentencia o instruccin de clculo, el resultado ser

124

Captulo 7. Tipos de dato y variables en C.

siempre el valor 0 para la variable superficie, sea cual sea el valor actual de las variable base o altura: y es que al calcular el valor de 1 dividido entre 2, el procesador (en concreto la Unidad Aritmtico Lgica) ofrece como resultado el valor 0. Cuando el cociente se realiza entre variables de coma flotante, entonces el resultado es tambin de coma flotante. Siempre se debe evitar el cociente en el que el denominador sea igual a cero, porque en ese caso se dar un error de ejecucin y el programa quedar abortado. 5. Mdulo. El identificador de este operador es el carcter %. Este operador calcula el resto del cociente entero. Por su misma definicin, no tiene sentido su aplicacin entre variables no enteras: su uso con variables de coma flotante provoca error de compilacin. Como en el cociente, tampoco su divisor puede ser cero. 6. Incremento y decremento. Estos dos operadores no existen en otros lenguajes. El identificador de estos operadores son los caracteres ++ para el incremento, y para el decremento. Este operador es vlido para todos los tipos de dato primitivos de C. La expresin a++; es equivalente a la expresin a = a + 1;. Y la expresin a; es equivalente a la expresin a = a 1;. Estos operadores condensan, en uno sola expresin, un operador asignacin, un operador suma (o resta) y un valor literal: el valor 1. Y como se puede apreciar son operadores unarios: se aplican a una sola variable. Dnde se ubique el operador con respecto a la variable (operador delante de la variable, o prefijo; o detrs de la variable, o sufijo) tiene su importancia, porque vara su comportamiento dentro del total de la expresin. Por ejemplo, el siguiente cdigo

125

Informtica Aplicada. Programacin en Lenguaje C.

unsigned short int a, b = 2, c = 5; a = b + c++; modifica dos variables: por el operador asignacin, la variable a tomar el valor resultante de sumar los contenidos de b y c; y por la operacin incremento, que lleva consigo asociado otro operador asignacin, se incrementa en uno el valor de la variable c. Pero queda una cuestin abierta: Qu operacin se hace primero: incrementar c y luego calcular b + c para asignar su resultado a la variable a (a valdra en ese caso 8); o hacer primero la suma (en ese caso a quedara con el valor 7) y slo despus incrementar la variable c? En ambos casos, queda claro que la variable b no modifica su valor y que la variable c pasa de valer 5 a valer 6. Eso lo indicar la posicin del operador. Si el operador incremento (o decremento) precede a la variable, entonces se ejecuta antes de evaluar el resto de la expresin; si se coloca despus de la variable, entonces primero se evala la expresin donde est implicada la variable a incrementar o decrementar y slo despus se incrementa o decrementa esa variable. En el ejemplo antes sugerido, el operador est ubicado a la derecha de la variable c. Por lo tanto, primero se efecta la suma y la asignacin sobre a, que pasa a valer 7; y luego se incrementa la variable c, que pasa a valer 6. La variable b no modifica su valor. Por completar el ejemplo, si la expresin hubiera sido a = b + ++c; entonces, al final tendramos que c vale 6 y que a vale 8, puesto que no se realizara la suma y la asignacin sobre a hasta despus de haber incrementado el valor de la variable c. Los operadores incremento y decremento, y el juego de la precedencia, son muy cmodos y se emplean mucho en los cdigos escritos en lenguaje C.

126

Captulo 7. Tipos de dato y variables en C.

Aunque hasta el tema siguiente no se va a ver el modo en que se pueden recibir datos desde el teclado (funcin scanf) y el modo de mostrar datos por pantalla (funcin printf), vamos a recoger a lo largo de este captulo algunas cuestiones muy sencillas para resolver. Por ahora lo importante no es entender el programa entero, sino la parte que hace referencia a la declaracin y uso de las variables.

Una breve consideracin sobre el cociente de enteros.


El estndar C90 define el resultado de la divisin de dos enteros, en el supuesto de que ese cociente no sea exacto, como el mayor entero menor que el cociente algebraico. Y esto, sea cual sea el signo de cada uno de los dos enteros del cociente: sea pues el resultado menor o mayor que cero. El estndar C99 cambia esa definicin; en este nuevo estndar, el resultado de la divisin de dos enteros, en el supuesto de que ese cociente no sea exacto, es el cociente algebraico al que se le ha eliminado la parte fraccional: lo que se conoce habitualmente como truncado hacia el cero. Y eso, de nuevo, independientemente de cul sea el signo de la operacin. Cuando el resultado es positivo, entonces ambas definiciones coinciden. Pero cuando llegamos a un cociente no exacto de dos enteros de signos opuestos, los resultados difieren en la unidad. Por ejemplo: short a = -3 , b = 2, c; c = a / b; Cunto vale c? Si este programa se compila de acuerdo con el estndar C90, entonces el valor de la variable c es 2: el mayor entero menor que 1,5, que es el resultado algebraico del cociente, es 2. Si, en

127

Informtica Aplicada. Programacin en Lenguaje C.

cambio, se compila y ejecuta de acuerdo con el estndar C99, entonces el valor de la variable c es 1: el truncado del resultado algebraico 1,5 es 1. Al presentar los operadores desplazamiento a nivel de bit se podr comprobar que la definicin presentada en el estndar C90 ofrece una coherencia entre estos operadores. Pero a cambio ofrece algunos inconvenientes, que resuelve el estndar c99. El estndar C99 persigue otras ventajas con su nueva definicin. Gracias a la nueva definicin de cociente (truncado hacia el cero) se verifica que la expresin ((a/b) == (a)/b) && ((a/b) == a/(b)) sea siempre vlida, cosa que no permite la definicin del estndar C90. Cuando se programa hay que tener en mente estas pequeas

consideraciones sobre las definiciones de los operadores.

Operadores relacionales y lgicos.


Los operadores relacionales y los operadores lgicos crean expresiones que se evalan como verdaderas o falsas. En muchos lenguajes existe un tipo de dato primitivo para estos valores booleanos de verdadero o falso. En C ese tipo de dato no existe. El lenguaje C toma como falsa cualquier expresin que se evale como 0. Y toma como verdadera cualquier otra evaluacin de la expresin. Y cuando en C se evala una expresin con operadores relacionales y/o lgicos, la expresin queda evaluada a 0 si el resultado es falso; y a 1 si el resultado es verdadero. Los operadores relacionales son seis: igual que (==), distintos (!=) , mayor que (>), mayor o igual que (>=), menor que (<) y menor o igual que (<=). Todos ellos se pueden aplicar a cualquier tipo de dato primitivo de C.

128

Captulo 7. Tipos de dato y variables en C.

Una expresin con operadores relacionales sera, por ejemplo, a != 0, que ser verdadero si a toma cualquier valor diferente al 0, y ser falso si a toma el valor 0. Otras expresiones relacionales seran, por ejemplo, a > b + 2; x + y == z + t; Con frecuencia interesar evaluar una expresin en la que se obtenga verdadero o falso no slo en funcin de una relacin, sino de varias. Por ejemplo, se podra necesitar saber (obtener verdadero o falso) si el valor de una variable concreta est entre dos lmites superior e inferior. Para eso necesitamos concatenar dos relacionales. Y eso se logra mediante los operadores lgicos. Un error frecuente (y de graves consecuencias en la ejecucin del programa) al programar en C C++ es escribir el operador asignacin (=), cuando lo que se pretenda escribir era el operador relacional igual que (==). En los lenguajes C C++ la expresin variable = valor; ser siempre verdadera si valor es distinto de cero. Si colocamos una asignacin donde desebamos poner el operador relacional igual que, tendremos dos consecuencias graves: se cambiar el valor de la variable colocada a la izquierda del operador asignacin (cosa que no queramos) y, si el valor de la variable de la derecha es distinto de cero, la expresin se evaluar como verdadera al margen de cules fueran los valores iniciales de las variables. Los operadores lgicos son: AND, cuyo identificador est formado por el carcter repetido &&; OR, con el identificador ||; y el operador negacin, cuyo identificador es el carcter de admiracin final (!). Estos operadores binarios actan sobre dos expresiones que sern verdaderas (o distintas de cero), o falsas (o iguales a cero), y devuelven como valor 1 0 dependiendo de que la evaluacin haya resultado verdadera o falsa.

129

Informtica Aplicada. Programacin en Lenguaje C.

La tabla de valores para conocer el comportamiento de estos operadores est recogida en la tabla 7.2. En esa tabla se recoge el resultado de los tres operadores en funcin del valor de cada una de las dos expresiones que evalan.

a F F V V

b F V F V

a && b F F F V

a || b F V V V

!a V V F F

Tabla 7.2.: Resultados de los operadores lgicos.

Por ejemplo, supongamos el siguiente cdigo en C: int a = 1 , b = 3 , x = 30 , y = 10; int resultado; resultado = a * x == b * y; El valor de la variable resultado quedar igual a 1. Y si queremos saber si la variable x guarda un valor entero positivo menor que cien, escribiremos la expresin (x > 0 && x < 100) Con estos dos grupos de operadores son muchas las expresiones de evaluacin que se pueden generar. Quiz en este momento no adquiera mucho sentido ser capaz de expresar algo as; pero ms adelante se ver cmo la posibilidad de verificar sobre la veracidad y falsedad de muchas expresiones permite crear estructuras de control condicionales, o de repeticin. Una expresin con operadores relacionales y lgicos admite varias formas equivalentes Por ejemplo, la antes escrita sobre el intervalo de situacin del valor de la variable x es equivalente a escribir

130

Captulo 7. Tipos de dato y variables en C.

!(x < 0 || x >= 100) Evaluar las siguientes expresiones. short a = 0, b = 1, c = 5; a; // FALSO b; // VERDADERO a < b; // VERDADERO 5 * (a + b) == c; // VERDADERO

float pi = 3.141596; long x = 0, y = 100, z =1234; 3 * pi < y && (x + y) * 10 <= z / 2; 3 * pi < y || (x + y) * 10 <= z / 2; 3 * pi < y && !((x + y) * 10 <= z / 2); // FALSO // VERDADERO // VERDADERO

long a = 5, b = 25, c = 125, d = 625; 5 * a == b; // VERDADERO 5 * b == c; // VERDADERO a + b + c + d < 1000; // VERDADERO a > b || a = 10; // VERDADERO La ltima expresin trae su trampa: Por su estructura se ve que se ha pretendido crear una expresin lgica formada por dos sencillas enlazadas por el operador OR. Pero al establecer que uno de los extremos de la condicin es a = 10 (asignacin, y no operador relacional igual que) se tiene que en esta expresin recogida la variable a pasa a valer 10 y la expresin es verdadera puesto que el valor 10 es verdadero (todo valor distinto de cero es verdadero).

Operadores a nivel de bit.


Ya se ha dicho en el captulo cuarto que el lenguaje C es de medio nivel. Con eso se quiere decir que es un lenguaje de programacin que tiene la capacidad de trabajar a muy bajo nivel, modificando un bit de una variable, o logrando cdigos que manipulan la codificacin interna de la informacin. Eso se logra gracias a los operadores de nivel de bit.

131

Informtica Aplicada. Programacin en Lenguaje C.

Todos los operadores a nivel de bit estn definidos nicamente sobre variables de tipo entero. No se puede aplicar sobre una variable float, ni sobre una double, ni sobre una long double. Si se utilizan con variables de esos tipos, se produce un error de compilacin. Los operadores a nivel de bit son seis: 1. Operador AND a nivel de bit. Su identificador es un solo carcter &. Se aplica sobre variables del mismo tipo, con la misma longitud de bits. Bit a bit compara los dos de cada misma posicin y asigna al resultado un 1 en ese bit en esa posicin si los dos bits de las dos variables sobre las que se opera valen 1; en otro caso asigna a esa posicin del bit el valor 0. 2. Operador OR a nivel de bit. Su identificador es un solo carcter |. Se aplica sobre variables del mismo tipo, con la misma longitud de bits. Bit a bit compara los dos de cada misma posicin y asigna al resultado un 1 en ese bit en esa posicin si alguno de los dos bits de las dos variables sobre las que se opera valen 1; si ambos bits valen cero, asigna a esa posicin del bit el valor 0. Es frecuente en C y C++ el error de pretender escribir el operador lgico and (&&), o el or (||) y escribir finalmente el operador a nivel de bit (& |). Desde luego el significado de la sentencia o instruccin ser completamente distinto e imprevisible. Ser un error del programa de difcil deteccin. 3. Operador OR EXCLUSIVO, XOR a nivel de bit. Su identificador es un carcter ^. Se aplica sobre variables del mismo tipo, con la misma longitud de bits. Bit a bit compara los dos de cada misma posicin y asigna al resultado un 1 en ese bit en esa posicin si los dos bits de las dos variables tienen valores distintos: el uno es 1 y el otro 0, o viceversa; si los dos bits son iguales (dos unos o dos ceros), asigna a esa posicin del bit el valor 0.

132

Captulo 7. Tipos de dato y variables en C.

Por ejemplo, y antes de seguir con los otros tres operadores a nivel de bit, supongamos que tenemos el siguiente cdigo: unsigned unsigned unsigned unsigned short short short short int int int int a = 0xABCD, b = 0x6789; a_and_b = a & b; a_or_b = a | b; a_xor_b = a ^ b;

La variable a vale, en hexadecimal ABCD, y en decimal 43981. La variable b 6789, que en base diez es 26505. Para comprender el comportamiento de estos tres operadores, se muestra ahora en la tabla 7.3. los valores de a y de b en base dos, donde se puede ver bit a bit de ambas variables, y veremos tambin el bit a bit de las tres variables calculadas.

variable a b a_and_b a_or_b a_xor_b

binario 1010 0110 0010 1110 1100 1011 0111 0011 1111 1100 1100 1000 1000 1100 0100 1101 1001 1001 1101 0100

hex. ABCD 6789 2389 EFCD CC44

dec. 43981 26505 9097 61389 52292

Tabla 7.3.: Valores del ejemplo en binario, hexadecimal y decimal. Operadores a nivel de bit.

En la variable a_and_b se tiene un 1 en aquellas posiciones de bit donde haba 1 en a y en b; un 0 en otro caso. En la variable a_or_b se tiene un 1 en aquellas posiciones de bit donde haba al menos un 1 entre a y b; un 0 en otro caso. En la variable a_xor_b se tiene un 1 en aquellas posiciones de bit donde haba un 1 en a y un 0 en b, o un 0 en a y un 1 en b; y un cero cuando ambos bits coincidan de valor en esa posicin. La tabla de valores de estos tres operadores queda recogida en la tabla 7.4.

133

Informtica Aplicada. Programacin en Lenguaje C.

and 0 0 1 1 0 1 0 1 0 0 0 1

or 0 1 1 1

xor 0 1 1 0

Tabla 7.4.: Valores que adoptan los tres operadores a nivel de bit

4. Operador complemento a uno. Este operador unario devuelve el complemento a uno del valor de la variable a la que se aplica (cfr. Captulo 2: recuerde que en la base binaria, el complemento a uno de un nmero se obtiene sin ms que cambiar los ceros por unos, y los unos por ceros.). Su identificador es el carcter ~. Si se tiene que a la variable x de tipo short se le asigna el valor hexadecimal ABCD, entonces la variable y, a la que se asigna el valor ~x valdr 5432. O si x vale 578D, entonces y valdr A872. Puede verificar estos resultados calculando los complementos a uno de ambos nmeros. 5. Operador desplazamiento a izquierda. Su identificador es la cadena <<. Es un operador binario, que realiza un desplazamiento de todos los bits de la variable o valor literal sobre la que se aplica un nmero dado de posiciones hacia la izquierda. Los bits ms a la izquierda (los ms significativos) se pierden; a la derecha se van introduciendo tantos bits puestos a cero como indique el desplazamiento. Por ejemplo, si tenemos el siguiente cdigo: short int var1 = 0x7654; short int var2 = var1 << 3;

134

Captulo 7. Tipos de dato y variables en C.

La variable var2 tendr el valor de la variable var1 a la que se le aplica un desplazamiento a izquierda de 3 bits. Si la variable var1 tiene el valor, en base binaria 0111 (7) 0110 (6) 0101 (5) 0100 (4) (estado de la variable var1)

entonces la variable var2, a la que se asigna el valor de la variable var1 al que se han aadido tres ceros a su derecha y se le han eliminado los tres dgitos ms a la izquierda queda: 1011 (B) 0010 (2) 1010 (A) 0000 (0) (estado de la variable var2)

Es decir, var2 valdr, en hexadecimal, B2A0. Una observacin sobre esta operacin. Introducir un cero a la derecha de un nmero es lo mismo que multiplicarlo por la base. En el siguiente cdigo unsigned short int var1 = 12; unsigned short int d = 1; unsigned short int var2 = var1 << d; var2 ser el doble que var1, es decir, 24. Y si d hubiera sido igual a 2, entonces var2 sera cuatro veces var1, es decir, 48. Y si d hubiera sido igual a 3, entonces var2 sera ocho veces var1, es decir, 96. Si llega un momento en que el desplazamiento obliga a perder algn dgito 1 a la izquierda, entonces ya habremos perdido esa progresin, porque la memoria no ser suficiente para albergar todos sus dgitos y la cifra ser truncada. Si las variables var1 y var2 estn declaradas como signed, y si la variable var1 tiene asignado un valor negativo (por ejemplo, -7), tambin se cumple que el desplazamiento equivalga a multiplicar por dos. Es buen ejercicio de clculo de complementos y de codificacin de enteros con signo verificar los datos que a continuacin se presentan: var1 = -7;: estado de memoria FFF9

135

Informtica Aplicada. Programacin en Lenguaje C.

var2 = var1 << 1;: estado de memoria para la variable var2 ser FFF2, que es la codificacin del entero -14. 6. Operador desplazamiento a derecha. Su identificador es la cadena >>. Es un operador binario, que realiza un desplazamiento de todos los bits de la variable o valor literal sobre la que se aplica un nmero dado de posiciones hacia la derecha. Los bits ms a la derecha (los menos significativos) se pierden; a la izquierda se van introduciendo tantos bits como indique el desplazamiento. En esta ocasin, el valor de los bits introducidos por la izquierda depender del signo del entero sobre el que se aplica el operador desplazamiento. Si el entero es positivo, se introducen tantos ceros por la izquierda como indique el desplazamiento. Si el entero es negativo, se introducen tantos unos por la izquierda como indique el desplazamiento. Evidentemente, si el entero sobre el que se aplica el desplazamiento a derecha est declarado como unsigned, nicamente sern ceros lo que se introduzca por su izquierda, puesto que en ningn caso puede codificar un valor negativo. Si desplazar a la izquierda era equivalente a multiplicar por la base, ahora, desplazar a la derecha es equivalente a dividir por la base. Aqu hay que recordar las dos definiciones de cociente propuestas por los estndares C90 y C99. En ambas, si el entero sobre el que se realiza el desplazamiento no es negativo, el comportamiento es, efectivamente, el de dividir por 2. Pero cuando el entero sobre el que se realiza el desplazamiento es negativo (comienza por 1), entonces el resultado obtenido coincide con la definicin de cociente del estndar C90, pero no con la del estndar C99. Por ejemplo, si tenemos el siguiente cdigo signed short int var1 = -231; signed short int var2 = var1 >> 1; Entonces, el estado que codifica el valor de var1 es, expresado en hexadecimal, FF19. Y el valor que codifica entonces var2, si lo hemos

136

Captulo 7. Tipos de dato y variables en C.

desplazado un bit a la derecha, ser FF8C, que es la codificacin del entero negativo 116, que coincide con el resultado de dividir 231 entre 2 en C90, pero no en C99, cuyo valor final de ese cociente ser 115. 1111 (F) 1111 (F) 1111 (F) 1111 (F) 0001 (1) 1000 (8) 1001 (9) 1100 (C) (estado de la variable var1) (estado de la variable var2)

Los operadores a nivel de bit tienen una gran potencialidad. De todas formas no son operaciones a las que se est normalmente habituado, y eso hace que no resulte muchas veces evidente su uso. Los operadores a nivel de bit operan a mucha mayor velocidad que, por ejemplo, el operador producto o cociente. En la medida en que se sabe, quien trabaja haciendo uso de esos operadores puede lograr programas notablemente ms veloces en ejecucin.

Operadores compuestos.
Ya se ha visto que una expresin de asignacin en C trae, a su izquierda (Lvalue), el nombre de una variable y, a su derecha (Rvalue) una expresin a evaluar, o un literal, o el nombre de otra variable. Y ocurre frecuentemente que la variable situada a la izquierda forma parte de la expresin de la derecha. En estos casos, y si la expresin es sencilla, todos los operadores aritmticos y los operadores a nivel de bit binarios (exceptuando, por tanto, los operadores de signo, incremento, decremento y complemento) pueden presentar otra forma, en la que se asocia el operador con el operador asignacin. Son los llamados operadores de asignacin compuestos: += = *= /= %= >>= <<= x x x x x x x += y; -= y; *= y; /= y; %= y; >>= y; <<= y; es es es es es es es lo lo lo lo lo lo lo mismo mismo mismo mismo mismo mismo mismo que que que que que que que decir decir decir decir decir decir decir x x x x x x x = = = = = = = x x x x x x x + y; y; * y; / y; % y; >> y; << y;

137

Informtica Aplicada. Programacin en Lenguaje C.

&= |= ^=

x &= y; x |= y; x ^= y;

es lo mismo que decir x = x & y; es lo mismo que decir x = x | y; es lo mismo que decir x = x ^ y;

Tenga cuidado al usar esos operadores. Si lo que usted quiere expresar es que a = a b; entonces podr decir a = b;. Pero si lo que quiere expresar es a = b a; entonces no debe, en ese caso, utilizar el operador compuesto. Tenga en cuenta que las operaciones producto y suma son conmutativas, pero no lo son, en cambio, ni el cociente ni el resto, ni la resta. Puede parecer que estos operadores no facilitan la comprensin del cdigo escrito. Quiz una expresin de la forma F*=n; no tenga una presentacin muy clara. Pero de hecho estos operadores compuestos se usan frecuentemente y, quien se habita a ellos, agradece que se hayan definido para el lenguaje C. Por cierto, que la expresin del prrafo anterior es equivalente a escribir estas dos lneas de cdigo: F = F * n; y n = n 1;.

Operador sizeof.
Ya sabemos el nmero de bytes que ocupan en memoria todas las variables de tipo de dato primitivo en C: 1 byte las variables tipo char; 2 las de tipo short; 4 las de tipo long y float; 8 las de tipo double, y 10 las variables long double. Pero ya se ha dicho que adems de estos tipos primitivos, C permite la definicin de otros tipos diferentes, combinacin de esos primitivos. Y los tamaos de esos tipos definidos pueden ser tan diversos como diversas pueden ser las definiciones de esos nuevos tipos. No es extrao trabajar con tipos cuyas variables ocupan 13 bytes, 1045, cualquier otro tamao. C ofrece un operador que devuelve la cantidad de bytes que ocupa una variable o un tipo de dato concreto. El valor devuelto es tomado como

138

Captulo 7. Tipos de dato y variables en C.

un entero, y puede estar presente en cualquier expresin de C. Es el operador sizeof. Su sintaxis es: sizeof(nombre_variable); sizeof(nombre_tipo_de_dato); ya que se puede utilizar tanto con una variable concreta como indicndole al operador el nombre del tipo de dato sobre el que queramos conocer su tamao. No es vlido utilizar este operador indicando entre parntesis el tipo de dato void: esa instruccin dara error en tiempo de compilacin. Con este operador aseguramos la portabilidad, al no depender la aplicacin del tamao del tipo de datos de la mquina que se vaya a usar. Aunque ahora mismo no se ha visto en este texto qu utilidad puede tener en un programa conocer, como dato de clculo, el nmero de bytes que ocupa una variable, la verdad es que con frecuencia ese dato es muy necesario. Ejemplo: Podemos ver el tamao de los diferentes tipos de datos primitivos de C. Basta teclear este cdigo en nuestro editor: #include <stdio.h> main() { printf("int printf("char printf("short printf("long printf("float printf("double }

=> => => => => =>

%d\n",sizeof(int)); %d\n",sizeof(char)); %d\n",sizeof(short)); %d\n",sizeof(long)); %d\n",sizeof(float)); %d\n",sizeof(double));

(La funcin printf quedar presentada y explicada en el prximo captulo.)

Expresiones en las que intervienen variables de diferente tipo.


Hay lenguajes de programacin que no permiten realizar operaciones con valores de tipos de dato distintos. Se dice que son lenguajes de

139

Informtica Aplicada. Programacin en Lenguaje C.

tipado fuerte, que fuerzan la comprobacin de la coherencia de tipos en todas las expresiones, y lo verifican en tiempo de compilacin. El lenguaje C NO es de esos lenguajes, y permite la compilacin de un programa con expresiones que mezclan los tipos de datos. Y aunque en C se pueden crear expresiones en las que intervengan variables y literales de diferente tipo de dato, el procesador trabaja de forma que todas las operaciones que se realizan en la ALU sean con valores del mismo dominio. Para lograr eso, cuando se mezclan en una expresin diferentes tipos de dato, el compilador convierte todas las variables a un nico tipo compatible; y slo despus de haber hecho la conversin se realiza la operacin. Esta conversin en se una realiza de forma donde que no se pueda perder de

informacin:

expresin

intervienen

elementos

diferentes dominios, todos los valores se codifican de acuerdo con el tipo de dato de mayor rango. La ordenacin de rango de los tipos de dato primitivos de C es, de menor a mayor, la siguiente: char short long float double long double As, por ejemplo, si se presenta el siguiente cdigo: char ch = 7; short sh = 2; long ln = 100, ln2; double x = 12.4, y; y = (ch * ln) / sh x; La expresin para el clculo del valor que se almacenar finalmente en la variable y va cambiando de tipo de dato a medida que se realizan las operaciones y se introducen nuevas variables de nuevos tipos de dato: en el producto de la variable char con la variable long, se fuerza el cambio de la variable de tipo char, que se recodificar y as quedar para su uso en la ALU, a tipo long. Esa suma ser por tanto un valor

140

Captulo 7. Tipos de dato y variables en C.

long. Luego se realizar el cociente con la variable short, que deber convertirse en long para poder dividir al resultado long antes obtenido. Y, finalmente, el resultado del cociente se debe convertir en un valor double, para poder restarle el valor de la variable x. El resultado final ser pues un valor del tipo de dato double. Y as ser almacenado en la posicin de memoria de la variable y de tipo double. Si la ltima instruccin hubiese sido ln2 = (ch * ln) / sh x; todo hubiera sido como se ha explicado, pero a la hora de almacenar la informacin en la memoria reservada para la variable long ln2, el resultado final, que vena expresado en formato double, deber recodificarse para ser guardado como long. Y es que, en el trasiego de la memoria a los registros de la ALU, bien se puede hacer un cambio de tipo y por tanto un cambio de forma de codificacin y, especialmente, de nmero de bytes empleados para esa codificacin. Pero lo que no se puede hacer es que en una posicin de memoria como la del ejemplo, que dedica 32 bits a almacenar informacin, se quiera almacenar un valor de 64 bits, que es lo que ocupan las variables double. Ante el operador asignacin, si la expresin evaluada, situada en la parte derecha de la asignacin, es de un tipo de dato diferente al tipo de dato de la variable indicada a la izquierda de la asignacin, entonces el valor del lado derecho de la asignacin se convierte al tipo de dato del lado izquierdo. En este caso el forzado de tipo de dato puede consistir en llevar un valor a un tipo de dato de menor rango. Y ese cambio corre el riesgo de perder truncar la informacin.

Operador para forzar cambio de tipo.


En el epgrafe anterior se ha visto el cambio o conversin de tipo de dato que se realiza de forma implcita en el procesador cuando

141

Informtica Aplicada. Programacin en Lenguaje C.

encuentra expresiones que contienen diferentes tipos de dato. Tambin existe una forma en que programador puede forzar un cambio de tipo de forma explcita. Este cambio se llama cambio por promocin, o casting. C dispone de un operador para forzar esos cambios. La sintaxis de este operador unario es la siguiente: (tipo) nombre_variable; El operador de promocin de tipo, o casting, precede a la variable. Se escribe entre parntesis el nombre del tipo de dato hacia donde se desea forzar el valor codificado en la variable sobre la que se aplica el operador. La operacin de conversin debe utilizarse con cautelas, de forma que los cambios de tipo sean posibles y compatibles. No se puede realizar cualquier cambio. Especialmente cuando se trabaja con tipos de dato creados (no primitivos), que pueden tener una complejidad grande. Tambin hay que estar vigilante a los cambios de tipo que fuerzan a una disminucin en el rango: por ejemplo, forzar a que una variable float pase a ser de tipo long. El rango de valores de una variable float es mucho mayor, y si el valor de la variable es mayor que el valor mximo del dominio de los enteros de 4 bytes, entonces el resultado del operador forzar tipo ser imprevisible. Y tendremos entonces una operacin que no ofrece problema alguno en tiempo de compilacin, pero que bien puede llevar a resultados equivocados en tiempo de ejecucin. No se pueden realizar conversiones del tipo void a cualquier otro tipo, pero s de cualquier otro tipo al tipo void. Eso se entender mejor ms adelante. En la tabla 7.5. se muestran las posibles prdidas de informacin que se pueden producir en conversiones forzadas de tipo de dato. Esas prdidas se darn tanto si la conversin de tipo de dato viene forzada por el operador conversor de tipo, como si es debido a exigencias del operador

142

Captulo 7. Tipos de dato y variables en C.

asignacin. Evidentemente, salvo motivos intencionados que rara vez se han de dar, este tipo de no interrumpen conversiones el trabajo hay que de evitarlas. Los cuando compiladores compilacin

descubren, en el cdigo, alguna conversin de esta ndole, pero s advierten de aquellas expresiones donde puede haber un cambio forzado de tipo de dato que conduzca a la prdida de informacin.

de tipo char short long int long int float

al tipo signed char char char short int

Posibles prdidas Si el valor inicial es mayor de 127, entonces el nuevo valor ser negativo. Se pierden los 8 bits ms significativos. Se pierden los 24 bits ms significativos. Se pierden los 16 bits ms significativos. Se pierde la parte fraccional y ms informacin. Si esta parte entera es demasiado grande, se guardar su valor de forma truncada. Se pierde precisin. El resultado se presenta redondeado. Se pierde precisin. El resultado se presenta redondeado.

double long double

float double

Tabla 7.5.: Prdidas de informacin en los cambios de tipo con disminucin de rango.

Propiedades de los operadores.


Al evaluar una expresin formada por diferentes variables y literales, y por diversos operadores, hay que lograr expresar realmente lo que se desea operar. Por ejemplo, la expresin a + b * c Se evala como el

143

Informtica Aplicada. Programacin en Lenguaje C.

producto de la suma de a y b, con c; o se evala como la suma del producto de b con c, y a? Para definir unas reglas que permitan una interpretacin nica e inequvoca de cualquier expresin, se han definido tres propiedades en los operadores: 1. Su posicin. Establece dnde se coloca el operador con respecto a sus operandos. Un operador se llamar infijo si viene a colocarse entre sus operandos; y se llamar prefijo si el operador precede al operando. 2. Su precedencia. Establece el orden en que se ejecutan los distintos operadores implicados en una expresin. Existe un orden de precedencia perfectamente definido, de forma que en ningn caso una expresin puede tener diferentes interpretaciones. Y el compilador de C siempre entender las expresiones de acuerdo con su orden de precedencia establecido. 3. Su asociatividad. Esta propiedad resuelve la ambigedad en la eleccin de operadores que tengan definida la misma precedencia. En la prctica habitual de un programador, se acude a dos reglas para lograr escribir expresiones que resulten correctamente evaluadas: 1. Hacer uso de parntesis. De hecho los parntesis son un operador ms, que adems son los primeros en el orden de ejecucin. De acuerdo con esta regla, la expresin antes recogida podra escribirse (a + b) * c; a + (b * c); en funcin de cul de las dos se desea. Ahora, con los parntesis, estas expresiones no llevan a equvoco alguno. De todas formas, para el compilador tampoco antes, cuando no utilizamos los parntesis, haba ninguna ambigedad. 2. Conocer y aplicar las reglas de precedencia y de asociacin por izquierda y derecha. Este orden podr ser siempre alterado mediante el uso de parntesis. Segn esta regla, la expresin antes recogida se interpreta como a + (b * c).

144

Captulo 7. Tipos de dato y variables en C.

Se considera buena prctica de programacin conocer esas reglas de precedencia y no hacer uso abusivo de los parntesis. De todas formas, cuando se duda sobre cmo se evaluar una expresin, lo habitual es echar mano de los parntesis. A veces una expresin adquiere mayor claridad si se recurre al uso de los parntesis.

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

() ! .* * + << > == & ^ | && || ?: = ,

[] ~

-> ++

. -+ * &

ID DI ID ID ID

->* / >> >= != < <= %

ID ID ID ID ID ID ID DI ID

+=

-=

*=

/=

%=

&=

|=

<<=

>>=

DI ID

Tabla 7.6.: Precedencia y Asociatividad de los operadores.

Las reglas de precedencia son las que se recogen el la tabla 7.6. Cuanto ms alto en la tabla est el operador, ms alta es su precedencia, y antes se evala ese operador que cualquier otro que est ms abajo en la tabla. Y para aquellos operadores que estn en la misma fila, es decir,

145

Informtica Aplicada. Programacin en Lenguaje C.

que tengan el mismo grado de precedencia, el orden de evaluacin, en el caso en que ambos operadores intervengan en una expresin, viene definido por la asociatividad: de derecha a izquierda o de izquierda a derecha. Tenga en cuenta que todava no hemos presentado todos los operadores que existen en C. En la Tabla 7.6. se recogen todos los operadores: algunos, an no lo ha conocido. Existen 16 categoras de precedencia, y todos los operadores colocados en la misma categora tienen igual precedencia que cualquiera otro de la misma categora. Algunas de esas categoras tan solo tienen un operador. Cuando un operador viene duplicado en la tabla, la primera ocurrencia es como operador unario, la segunda como operador binario. Cada categora tiene su regla de asociatividad: de derecha a izquierda (anotada como D I), o de izquierda a derecha (anotada como I D). Por ejemplo, la expresin a * x + b * y c / z: se evala en el siguiente orden: primero los productos y el cociente, y ya luego la suma y la resta. Todos estos operadores estn en categoras con asociatividad de izquierda a derecha, por lo tanto, primero se efecta el producto ms a la izquierda y luego el segundo, ms al centro de la expresin. Despus se efecta el cociente, que est ms a la derecha; luego la suma y finalmente la resta. Todos los operadores de la tabla 7.6. que faltan por presentar en el manual se emplean para vectores y cadenas y para operatoria de punteros. Ms adelante se conocern todos ellos.

Valores fuera de rango en una variable.


Ya hemos dicho repetidamente que una variable es un espacio de memoria, de un tamao concreto en donde la informacin se codifica de

146

Captulo 7. Tipos de dato y variables en C.

una manera determinada por el tipo de dato que se vaya a almacenar en esa variable. Espacio de memoria limitado. Es importante conocer los lmites de nuestras variables. Esos lmites ya venan presentados en la tabla 7.1. Cuando en un programa se pretende asignar a una variable un valor que no pertenece al dominio, el resultado es habitualmente extrao. Se suele decir que es imprevisible, pero la verdad es que la electrnica del procesador acta de la forma para la que est diseada, y no son valores aleatorios los que se alcanzan entonces, aunque s, muchas veces, valores no deseados, o valores errneos. Se muestra ahora el comportamiento de las variables ante un

desbordamiento (que as se le llama) de la memoria. Si la variable es entera, ante un desbordamiento de memoria el procesador trabaja de la misma forma que lo hace, por ejemplo, el cuenta kilmetros de un vehculo. Si en un cuenta kilmetros de cinco dgitos, est marcado el valor 99.998 kilmetros, al recorrer cinco kilmetros ms, el valor que aparece en pantalla ser 00.003. Se suele decir que se le ha dado la vuelta al marcador. Y algo similar ocurre con las variables enteras. En la tabla 7.7. se muestran los valores de diferentes operaciones con desbordamiento. Si el desbordamiento se realiza por asignacin directa, es decir, asignando a una variable un literal que sobrepasa el rango de su dominio, o asignndole el valor de una variable de rango superior, entonces se almacena el valor truncado. Por ejemplo, si a una variable unsigned short se le asigna un valor que requiere 25 dgitos binarios, nicamente se quedan almacenados los 16 menos significativos. A eso hay que aadirle que, si la variable es signed short, al tomar los 16 dgitos menos significativos, interpretar el ms significativo de ellos como el bit de signo, y segn sea ese bit, interpretar toda la

147

Informtica Aplicada. Programacin en Lenguaje C.

informacin codificada como entero negativo en complemento a la base, o como entero positivo.

signed short unsigned short signed long unsigned long

32767 + 1 da el valor 32768 65535 + 1 da el valor 0 2147483647 +1 da el valor -2147483648 4294967295 + 1 da el valor 0

Tabla 7.7.: valores de desbordamiento en las variables de tipo entero.

Hay

situaciones

problemas

donde

jugar

con

las

reglas

de

desbordamiento de enteros ofrece soluciones muy rpidas y buenas. Pero, evidentemente, en esos casos hay que saber lo que se hace. Si el desbordamiento es por asignacin, la variable entera desbordada almacenar un valor que nada tendr que ver con el original. Si el desbordamiento tiene lugar por realizar operaciones en un tipo de dato de coma flotante y en las que el valor final es demasiado grande para ese tipo de dato, entonces el resultado es completamente imprevisible, y posiblemente se produzca una interrupcin en la ejecucin del programa. Ese desbordamiento se considera, sin ms, error desbordamiento pudiera ser deseado. de programacin. No resulta sencillo pensar en una situacin en la que ese

Constantes (variables const). Directiva #define.


Cuando se desea crear una variable a la que se asigna un valor inicial que no debe modificarse, se la precede, en su declaracin, de la palabra clave de C const. const tipo var_1 = val_1[,var_2 = val_2, , var_N = val_N];

148

Captulo 7. Tipos de dato y variables en C.

Se declara con la palabra reservada const. Pueden definirse constantes de cualquiera de los tipos de datos simples. const float DOS_PI = 6.28; Como no se puede modificar su valor, la variable declarada como constante no podr estar en una asignacin, en la parte izquierda de sta (excepto en el momento de su inicializacin). Si se intenta modificar el valor de la variable declarada como constante mediante el operador asignacin el compilador dar un error y no se crear el programa. Otro modo de definir constantes es mediante la directiva de

preprocesador o de compilacin #define. Ya se ha visto en el captulo cuatro otra directiva que se emplea para indicar al compilador de qu bibliotecas se toman funciones ya creadas y compiladas: la directiva #include. Un ejemplo sencillo de uso de esta nueva directiva es el siguiente: #define DOS_PI 6.28 Como se ve, la directiva #define no termina en punto y coma. Eso es debido a que las directivas de compilacin no son instrucciones de C, sino rdenes que se dirigen al compilador. La directiva #define se ejecuta previamente a la compilacin, y sustituye, en todas las lneas de cdigo posteriores a la aparicin de la directiva, cada aparicin de la primera cadena de caracteres por la segunda cadena: en el ejemplo antes presentado, la cadena DOS_PI por el valor 6.28. Ya se ver cmo esta directiva pude resultar muy til en ocasiones.

Los enteros muy largos y otras consideraciones adicionales sobre tipos de dato en C90 y C99.
El nuevo estndar C99 admite, adems de todos los tipos presentados hasta el momento, algunos otros nuevos tipos. Entre otros, define el

149

Informtica Aplicada. Programacin en Lenguaje C.

tipo de dato booleano y el tipo complejo. No vamos a presentar stos aqu. Ahora nos centramos nicamente en la ampliacin que ofrece C99 para los enteros y las definiciones de sus lmites de rango de valores, muy til a la hora de programar. C99 ofrece cinco tipos de dato enteros diferentes: signed char, short int, int, long int y long long int. El tamao de una variable signed char es el mismo que el de una variable char. El tamao de una variable int depender de la arquitectura del entorno de ejecucin. Su rango de valores, como se ver en breve, queda definido como cualquier valor entre INT_MIN e INT_MAX. El estndar C99 ha introducido un nuevo tipo de dato entero, de mayor longitud que el que hasta este momento se tena: el tipo long long, de 64 bits (8 bytes). Para indicar que un literal expresa un valor de tipo long long se coloca el sufijo ll, LL, llu, LLU. Existe un archivo de cabecera, ya disponible en el estndar C90: <limits.h>, que recoge todos los lmites definidos para los rangos de valores o dominios de los tipos de datos enteros y estndares en C. Los tamaos recogidos en <limits.h> son los siguientes: Nmero de bits para el menor objeto de memoria #define CHAR_BIT 8 Valor mnimo para un objeto de tipo signed char: #define SCHAR_MIN (-128) Valor mximo para un objeto de tipo signed char: #define SCHAR_MAX 127 Valor mximo para un objeto de tipo unsigned char #define UCHAR_MAX 255 Valor mnimo para un objeto de tipo char #define CHAR_MIN SCHAR_MIN Valor mximo para un objeto de tipo char #define CHAR_MAX SCHAR_MAX Mximo valor de un objeto tipo long int

150

Captulo 7. Tipos de dato y variables en C.

#define LONG_MAX

2147483647

Mnimo valor de un objeto tipo long int #define LONG_MIN (-LONG_MAX-1) Mximo valor de un objeto tipo unsigned long int #define ULONG_MAX 0xffffffff Mximo valor de un objeto tipo short int #define SHRT_MAX 32767 Mnimo valor de un objeto tipo short int #define SHRT_MIN (-SHRT_MAX-1) Mximo valor de un objeto tipo unsigned short int #define USHRT_MAX 0xffff Mximo valor de un objeto tipo long long int #define LLONG_MAX 9223372036854775807LL Mnimo valor de un objeto tipo long long int #define LLONG_MIN (-LLONG_MAX - 1) Mximo valor de un objeto tipo unsigned long long int #define ULLONG_MAX (2ULL * LLONG_MAX + 1) Mximo valor de un objeto tipo int (dependiendo de la arquitectura) #define INT_MAX #define INT_MAX SHRT_MAX LONG_MAX

Mnimo valor de un objeto tipo int (dependiendo de la arquitectura) #define INT_MIN #define INT_MIN SHRT_MIN LONG_MIN

Mximo valor de un objeto tipo unsigned int (dependiendo de la arquitectura) #define UINT_MIN #define UINT_MIN USHRT_MIN ULONG_MIN

Por otro lado, C99 ofrece un archivo de cabecera, <stdint.h>, que recoge una coleccin de definiciones, tiles a la hora de programar con enteros. Declara un conjunto de tipos de dato enteros en especificando de forma explcita su tamao. Tambin define una coleccin de macros que marcan los lmites de estos nuevos tipos de dato enteros.

151

Informtica Aplicada. Programacin en Lenguaje C.

Los tipos de dato enteros en este archivo, vienen definidos de acuerdo a las siguientes categoras: Tipos enteros que tienen una anchura (en bytes) concreta y determinada. El formato genrico para expresar cualquiera de estos tipos de dato es el siguiente: intN_t, uintN_t, donde N indicar el nmero de bits y donde la u indica que el tipo de dato es sin signo. int8_t uint8_t int16_t uint16_t int32_t uint32_t int64_t uint64_t 1 1 2 2 4 4 8 8 byte byte byte byte byte byte byte byte con signo sin signo con signo sin signo con signo sin signo con signo sin signo

Tipos enteros que tienen, al menos, una anchura (en bytes) determinada. Estos tipos de datos garantizan un tamao mnimo de entero, independientemente del sistema en que se compile el programa. Si, por ejemplo, trabajramos en un sistema que proporcionase slo tipos uint32_t y uint64_t, entonces el tipo uint_least16_t ser equivalente a uint32_t. El formato genrico para expresar cualquiera de estos tipos de dato es el siguiente; int_leastN_t, uint_leastN_t, donde N indicar el nmero de bits y donde la u indica que el tipo de datos es sin signo. int_least8_t uint_least8_t int_least16_t uint_least16_t int_least32_t uint_least32_t int_least64_t uint_least64_t 1 1 2 2 4 4 8 8 byte byte byte byte byte byte byte byte con signo sin signo con signo sin signo con signo sin signo con signo sin signo

Los tipos enteros ms rpidos que tienen, al menos, una anchura (en bytes) determinada. Con este tipo de dato el compilador trata de establecer, dentro de las condiciones del tipo (anchura y signo) cul

152

Captulo 7. Tipos de dato y variables en C.

es el ptimo en relacin a su velocidad dentro del sistema en que se compila el programa. El formato genrico para expresar cualquiera de estos tipos de dato es el siguiente; int_fastN_t, uint_fastN_t, donde N indicar el nmero de bits y donde la u indica que el tipo de datos es sin signo. int_fast8_t uint_fast8_t int_fast16_t uint_fast16_t int_fast32_t uint_fast32_t int_fast64_t uint_fast64_t 1 1 2 2 4 4 8 8 byte byte byte byte byte byte byte byte con signo sin signo con signo sin signo con signo sin signo con signo sin signo

Los tipos enteros de mayor tamao. intmax_t uintmax_t es el entero con signo de mayor tamao. es el entero sin signo de mayor tamao.

El archivo <stdint.h> recoge tambin la definicin de lmites respecto a estos nuevos tipos de dato (semejante a la recogida en limits.h: Los lmites para los valores del tipo de dato entero de tamao exacto: #define #define #define #define #define #define #define #define #define #define #define #define INT8_MIN INT16_MIN INT32_MIN INT64_MIN INT8_MAX INT16_MAX INT32_MAX INT64_MAX UINT8_MAX UINT16_MAX UINT32_MAX UINT64_MAX (-128) (-32768) (-2147483647 - 1) (-9223372036854775807LL - 1) 127 32767 2147483647 9223372036854775807LL 0xff /* 255U */ 0xffff /* 65535U */ 0xffffffff /* 4294967295U */ 0xffffffffffffffffULL /* 18446744073709551615ULL */

Los lmites para los valores del tipo de dato de tamao al menos dependern evidentemente del sistema para el que se haya definido el compilador. Si suponemos un sistema que soporta todos los tamaos: 8, 16, 32 y 64, esos valores sern:

153

Informtica Aplicada. Programacin en Lenguaje C.

#define #define #define #define #define #define #define #define #define #define #define #define

INT_LEAST8_MIN INT_LEAST16_MIN INT_LEAST32_MIN INT_LEAST64_MIN INT_LEAST8_MAX INT_LEAST16_MAX INT_LEAST32_MAX INT_LEAST64_MAX UINT_LEAST8_MAX UINT_LEAST16_MAX UINT_LEAST32_MAX UINT_LEAST64_MAX

INT8_MIN INT16_MIN INT32_MIN INT64_MIN INT8_MAX INT16_MAX INT32_MAX INT64_MAX UINT8_MAX UINT16_MAX UINT32_MAX UINT64_MAX

Los lmites para los valores del tipo de dato de tamao los ms rpidos tambin dependern evidentemente del sistema para el que se haya definido el compilador. Si suponemos un sistema que soporta todos los tamaos: 8, 16, 32 y 64, esos valores sern: #define #define #define #define #define #define #define #define #define #define #define #define INT_FAST8_MIN INT_FAST16_MIN INT_FAST32_MIN INT_FAST64_MIN INT_FAST8_MAX INT_FAST16_MAX INT_FAST32_MAX INT_FAST64_MAX UINT_FAST8_MAX UINT_FAST16_MAX UINT_FAST32_MAX UINT_FAST64_MAX INT8_MIN INT16_MIN INT32_MIN INT64_MIN INT8_MAX INT16_MAX INT32_MAX INT64_MAX UINT8_MAX UINT16_MAX UINT32_MAX UINT64_MAX

Los lmites para los valores del tipo de dato de tamao mayor: #define INTMAX_MIN #define INTMAX_MAX #define UINTMAX_MAX INT64_MIN INT64_MAX UINT64_MAX

Intercambio de valores de dos variables.


Una operacin bastante habitual en un programa es el intercambio de valores entre dos variables. Supongamos el siguiente ejemplo: <variable1, int, , 10> y <variable2, int, , 20>

Si queremos que variable1 almacene el valor de variable2 y que

154

Captulo 7. Tipos de dato y variables en C.

variable2 almacene el de variable1, es necesario acudir a una variable auxiliar. El proceso es as: auxiliar = variable1; variable1 = variable2; variable2 = auxiliar; Porque no podemos copiar el valor de variable2 en variable1 sin perder con esta asignacin el valor de variable1 que queramos guardar en variable2. Con el operador or exclusivo se puede hacer intercambio de valores sin acudir, para ello, a una variable auxiliar. El procedimiento es el siguiente: variable1 = variable1 ^ variable2; variable2 = variable1 ^ variable2; variable1 = variable1 ^ variable2; que, con operadores compuestos queda de la siguiente manera: variable1 ^= variable2; variable2 ^= variable1; variable1 ^= variable2; Veamos un ejemplo para comprobar que realmente realiza el

intercambio: short int variable1 = 3579; short int variable2 = 2468; en base binaria el valor de las dos variables es: variable1 variable2 variable1 ^= variable2 variable2 ^= variable1 variable1 ^= variable2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 1 1 1 0 1 1 0 0 0 0 0 0 1 1 0 1 1 1 1 0 1 1 1 0 1 1 0 1 1 0 1 1 1 0 1 1 0 1 0 1 1 0 0 1 1 0 1 1 0 1 1 0 1 0 1 1 0

Al final del proceso, el valor de variable1 es el que inicialmente tena variable2, y al revs. Basta comparar valores. Para verificar que las operaciones estn correctas (que lo estn) hay que tener en cuenta que el proceso va cambiando los valores de variable1 y de variable2, y esos cambios hay que tenerlos en cuenta en las siguientes operaciones or exclusivo a nivel de bit.

155

Informtica Aplicada. Programacin en Lenguaje C.

Ayudas On line.
Muchos editores y compiladores de C cuentan con ayudas en lnea abundante. Todo lo referido en este captulo puede encontrarse en ellas. Es buena prctica de programacin saber manejarse por esas ayudas, que llegan a ser muy voluminosas y que gozan de buenos ndices para lograr encontrar el auxilio necesario en cada momento.

Recapitulacin.
Despus de estudiar este captulo, ya sabemos crear y operar con nuestras variables. Tambin conocemos muchos de los operadores definidos en C. Con todo esto podemos realizar ya muchos programas sencillos. Si conocemos el rango o dominio de cada tipo de dato, sabemos tambin de qu tipo conviene que sea cada variable que necesitemos. Y estaremos vigilantes en las operaciones que se realizan con esas variables, para no sobrepasar ese dominio e incurrir en un overflow. Tambin hemos visto las reglas para combinar, en una expresin, variables y valores de diferente tipo de dato. Es importante conocer bien todas las reglas que gobiernan estas combinaciones porque con frecuencia, si se trabaja sin tiento, se llegan a resultados errneos. Con los ejercicios que se proponen a continuacin se pueden practicar y poner a prueba nuestros conceptos adquiridos.

156

Captulo 7. Tipos de dato y variables en C.

Ejemplos y ejercicios propuestos.

7.1.

Escribir un programa que realice las operaciones de suma, resta, producto, cociente y mdulo de dos enteros introducidos por teclado.

#include <stdio.h> int main(void) { signed long a, b; signed long sum, res, pro, coc, mod; printf("Introduzca el valor del 1er. operando ... "); scanf("%ld",&a); printf("Introduzca el valor del 2do. operando ... "); scanf("%ld",&b); // Clculos sum = res = pro = coc = mod = a a a a a + * / % b; b; b; b; b;

// Mostrar resultados por pantalla. printf("La suma es igual a %ld\n", sum); printf("La resta es igual a %ld\n", res); printf("El producto es igual a %ld\n", pro); printf("El cociente es igual a %ld\n", coc); printf("El resto es igual a %ld\n", mod); return 0; } Observacin: cuando se realiza una operacin de cociente o de resto es muy recomendable antes verificar que, efectivamente, el divisor no es igual a cero. An no sabemos hacer esta operacin de verificacin, as pues, el programa queda un poco cojo. Al ejecutarlo ser importante que el usuario no introduzca un valor para la variable b igual a cero.

157

Informtica Aplicada. Programacin en Lenguaje C.

7.2.

Repetir el mismo programa para nmeros de coma flotante.

#include <stdio.h> int main(void) { float a, b; float sum, res, pro, coc; printf("Introduzca el valor del 1er. operando ... "); scanf("%f",&a); printf("Introduzca el valor del 2do. operando ... "); scanf("%f",&b); // Clculos sum = res = pro = coc = a a a a + * / b; b; b; b;

// mod = a % b; : esta operacin no est permitida // Mostrar resultados por pantalla. printf("La suma es igual a %f\n", sum); printf("La resta es igual a %f\n", res); printf("El producto es igual a %f\n", pro); printf("El cociente es igual a %f\n", coc); return 0; } En este caso se ha tenido que omitir la operacin mdulo, que no est definida para valores del dominio de los nmeros de coma flotante. Al igual que en el ejemplo anterior, se debera verificar (an no se han presentado las herramientas que lo permiten), antes de realizar el cociente, que el divisor era diferente de cero.

7.3.

Escriba un programa que solicite un entero y muestre por pantalla su valor al cuadrado y su valor al cubo.

158

Captulo 7. Tipos de dato y variables en C.

#include <stdio.h> int main(void) { short x; long cuadrado, cubo; printf("Introduzca un valor ... "); scanf("%hi",&x); cuadrado = (long)x * x; cubo = cuadrado * x; printf("El cuadrado de %hd es %li\n",x, cuadrado); printf("El cubo de %hd es %li\n",x, cubo); return 0; } Es importante crear una presentacin cmoda para el usuario. No tendra sentido comenzar el programa por la funcin scanf, porque en ese caso el programa comenzara esperando un dato del usuario, sin aviso previo que le indicase qu es lo que debe hacer. En el siguiente captulo se presenta con detalle las dos funciones de entrada y salida por consola. La variable x es short. Al calcular el valor de la variable cuadrado forzamos el tipo de dato para que el valor calculado sea long y no se pierda informacin en la operacin. En el clculo del valor de la variable cubo no es preciso hacer esa conversin, porque ya la variable cuadrado es de tipo long. En esta ltima operacin no queda garantizado que no se llegue a un desbordamiento: cualquier valor de x mayor de 1290 tiene un cubo no codificable con 32 bits. Se puede probar qu ocurre introduciendo valores mayores que ste indicado: comprobar que el programa errneos, ejecutar normalmente pero ofrecer unos resultados

159

Informtica Aplicada. Programacin en Lenguaje C.

7.4.

Escriba un programa que solicite los valores de la base y de la altura de un tringulo y que imprima por pantalla el valor de la superficie.

#include <stdio.h> int main(void) { double b, h, S; printf("Introduzca la base ... "); scanf("%lf",&b); printf("Introduzca la altura ... "); scanf("%lf",&h); S = b * h / 2; printf("La superficie del triangulo de "); printf("base %.2lf y altura %.2lf ",b,h); printf("es %.2lf",S); return 0; } Las variables se han tomado double. As no se pierde informacin en la operacin cociente. Puede probar a declarar las variables como de tipo short, modificando tambin algunos parmetros de las funciones de entrada y salida por consola: int main(void) { short b, h, S; printf("Introduzca la base ... "); scanf("%hd",&b); printf("Introduzca la altura ... "); scanf("%hd",&h); S = b * h / 2; printf("La superficie del triangulo de "); printf("base %hd y altura %hd ",b,h); printf("es %hd",S); return 0; }

160

Captulo 7. Tipos de dato y variables en C.

Si al ejecutar el programa el usuario introduce los valores 5 para la base y 3 para la altura, el valor de la superficie que mostrar el programa al ejecutarse ser ahora de 7, y no de 7,5.

7.5.

Escriba un programa que solicite el valor del radio y muestre la longitud de su circunferencia y la superficie del crculo inscrito en ella.

#include <stdio.h> #define PI 3.14159 int main(void) { signed short int r; double l, S; const double pi = 3.14159; printf("Indique el valor del radio ... "); scanf("%hd",&r); printf("La longitud de la circunferencia"); printf(" cuyo radio es %hd",r); l = 2 * pi * r; printf(" es %lf. \n",l); printf("La superficie de la circunferencia"); printf(" cuyo radio es %hd",r); S = PI * r * r; printf(" es %lf. \n",S); return 0; } En este ejemplo hemos mezclado tipos de dato. El radio lo tomamos como entero. Luego, en el clculo de la longitud l, como la expresin tiene el valor de la constante pi, que es double, se produce una conversin implcita de tipo de dato, y el resultado final es double. Para el clculo de la superficie, en lugar de emplear la constante pi se ha tomado un identificador PI definido mediante la directiva #define. El

161

Informtica Aplicada. Programacin en Lenguaje C.

valor de PI tambin tiene forma de coma flotante, y el resultado ser por tanto de este tipo. A diferencia de los ejemplos anteriores, en ste hemos guardado los resultados obtenidos en variables.

7.6.

Escriba un programa que solicite el valor de la temperatura en grados Fahrenheit y muestre por pantalla el equivalente en grados Celsius. La ecuacin que define esta trasformacin es: Celsius = (5 / 9) (Fahrenheit 32).

#include <stdio.h> int main(void) { double fahr, cels; printf("Temperatura en grados Fahrenheit ... "); scanf("%lf",&fahr); cels = (5 / 9) * (fahr - 32); printf("La temperatura en grados Celsius "); printf("resulta ... %lf.",cels); return 0; } Tal y como est escrito el cdigo parece que todo ha de ir bien. Si ensayamos el programa con la entrada en grado Fahrenheit igual a 32, entonces la temperatura en Celsius resulta 0, que es correcto puesto que esas son las temperaturas, en las dos tablas, en las que se derrite el hielo. Pero, y si probamos con otra entrada?... Tambin da 0! Por qu? Pues porque (5 / 9) es una operacin cociente entre dos enteros, cuyo resultado es un entero: el truncado, es decir, el mayor entero menor que el resultado de la operacin; es decir, 0.

162

Captulo 7. Tipos de dato y variables en C.

Cmo se debera escribir la operacin? cels = (5 / 9.0) * (fahr - 32); cels = (5.0 / 9) * (fahr - 32); cels = ((double)5 / 9) * (fahr - 32); cels = (5 / (double)9) * (fahr - 32); cels = 5 * (fahr - 32) / 9; Hay muchas formas: lo importante es saber en todo momento qu operacin realizar la sentencia que escribimos. Hay que saber escribir expresiones que relacionan valores de distinto tipo para no perder nunca informacin por falta de rango al elegir mal uno de los tipos de alguno de los literales o variables.

7.7.

Rotaciones de bits dentro de un entero.

Una

operacin

que

tiene

uso

en

algunas

aplicaciones

de

tipo

criptogrficos es la de rotacin de un entero. Rotar un nmero x posiciones hacia la derecha consiste en desplazar todos sus bits hacia la derecha esas x posiciones, pero sin perder los x bits menos significativos, que pasarn a situarse en la parte ms significativa del nmero. Por ejemplo, el nmero a = 1101 0101 0011 1110 rotado 4 bits a la derecha queda 1110 1101 0101 0011, donde los cuatro bits menos significativos (1110) se han venido a posicionar en la izquierda del nmero. La rotacin hacia la izquierda es anlogamente igual, donde ahora los x bits ms significativos del nmero pasan a la derecha del nmero. El nmero a rotado 5 bits a la izquierda quedara: 1010 0111 1101 1010. Qu rdenes deberamos dar para lograr la rotacin de un entero, 5 bits a la izquierda?: unsigned short int a, b, despl;

163

Informtica Aplicada. Programacin en Lenguaje C.

a = 0xABCD; despl = 5; b = ((a << despl) | (a >> (8 * sizeof(short) - despl))); Veamos cmo funciona este cdigo: a: despl: a << despl: 8 * sizeof(short) despl: a >> 11: b: 1010 1011 5 0111 1001 8 * 2 5 0000 0000 0111 1001 1100 1101 1010 0000 = 11 0001 0101 1011 0101

Y para lograr la rotacin de bits a la derecha: unsigned short int a, b, despl; a = 0xABCD; despl = 5; b = ((a >> despl) | (a << (8 * sizeof(short) - despl)));

7.8.

Indicar el valor que toman las variables en las siguientes asignaciones.

int a = 10, b = 4, c, d; float x = 10.0 , y = 4.0, z, t, v; c = a/b; // c valdr 2 d = x/y; // d valdr 2 z = a/b; // z valdr 2.0 z = x / y; // z vale ahora 2.5 t = (1 / 2) * x; // t vale 0.0 v = (1.0 / 2) * x; // v vale 5.0 Justifique todas los valores de las asignaciones.

7.9.

Muestre la salida que, por pantalla, ofrecer el siguiente cdigo en C:

#include <stdio.h> int main(void) { short a = 15, b = 2;

164

Captulo 7. Tipos de dato y variables en C.

short c1 = a / b, c2 = (double)a / b; double d1 = a / b, d2 = (double)a / b; printf("c1 = %hd \t c2 = %hd", c1, c2); printf("\t d1 = %4.1lf \t d2 = %4.1lf", d1, d2); return 0; }

c1 = 7

c2 = 7

d1 =

7.0

d2 =

7.5

7.10.

Escribir el siguiente programa y justificar la salida que ofrece por pantalla.

#include <stdio.h> void main(void) { char a = 127; a++; printf("%hd", a); } La salida que se obtiene con este cdigo es128. Intente justificar por qu ocurre. No se preocupe si an no conoce el funcionamiento de la funcin printf. Verdaderamente la variable a ahora vale 128. Por qu?

7.11.

Sabra adivinar qu va a salir por pantalla antes de ejecutar el programa?

#include <stdio.h> int main(void) { short sh = 0x7FFF; long ln = 0x7FFFFFFF; printf("\nEl valor inicial de sh es ... %hd", sh); printf("\nEl valor inicial de ln es ... %ld", ln);

165

Informtica Aplicada. Programacin en Lenguaje C.

sh++; ln++; printf("\nEl valor final de sh es ... %hd", sh); printf("\nEl valor final de ln es ... %ld", ln); return 0; }

7.12.

Escribir un programa que indique cuntos bits y bytes ocupa una variable long.

La solucin es inmediata si se utiliza el operador sizeof. #include <stdio.h> int main(void) { short bits, bytes; bytes = sizeof(long); bits = 8 * bytes; printf("BITS = %hd - BYTES = %hd.", bits, bytes); return 0; }

7.13.

Explique las salidas que, por pantalla, ofrece el siguiente cdigo:

#include <stdio.h> int main(void) { char a = 'X', b = 'Y'; printf("\nvalor (caracter) de a: %c", a); printf("\nvalor (caracter) de b: %c", b);

166

Captulo 7. Tipos de dato y variables en C.

printf("\nvalor de a (en base 10): %hd", a); printf("\nvalor de b (en base 10): %hd", b); printf("\nvalor (caracter) de a + b: %c", a + b); printf("\nvalor (en base 10) de a + b: %hd", a + b); printf("\nvalor (caracter) de a+b + 5: %c", a + b + 5); printf("\nvalor (en base 10) de a+b+5: %hd", a+b + 5); return 0; } La salida que se obtiene por pantalla es: valor valor valor valor valor valor valor valor (caracter) de a: X (caracter) de b: Y de a (en base 10): 88 de b (en base 10): 89 (caracter) de a + b: (en base 10) de a + b: 177 (caracter) de a + b + 5: (en base 10) de a + b + 5: 182

Cuando con la funcin printf hemos impreso los valores de las variables y de las expresiones, hemos indicado algunas veces que queramos que ese valor se mostrara como un valor del dominio de los caracteres (as se lo indicbamos en la funcin printf al poner %c); otras veces hemos querido que el valor que se mostrara fuera un entero dentro del dominio de enteros con signo de 16 bits (as se lo indicbamos en la funcin printf al poner %hd). Por eso, a veces la salida por pantalla nos daba un carcter; y otras nos daba un valor numrico. Las cuatro primeras lneas de salida son, por tanto, sencillas de interpretar: el programa imprime el valor de las variables a y b, primero como valores del rango de caracteres ASCII, y luego como valores enteros: efectivamente, el carcter X tiene el valor ASCII 0x58, es decir, 88 en base 10; y el carcter Y tiene el valor ASCII 0x59, es decir, 89 en base 10. valor valor valor valor (caracter) de (caracter) de de a (en base de b (en base a: X b: Y 10): 88 10): 89

167

Informtica Aplicada. Programacin en Lenguaje C.

Las otras lneas se comportar de la misma forma, pero ahora no mostramos el valor de las variables a b, sino el valor de una expresin. A + b, tratado como suma de enteros, efectivamente, da el resultado 177; y a + b + 5 da el resultado 182. Las variables de tipo char se comportan, en todo momento, como variables enteras de 8 bits. Otra cosa es cul sea el carcter que codifica ese nuevo valor 177 182: estamos en la zona extendida de la tabla de caracteres ASCII: el 177 es una carcter grfico (); el 182 es un carcter alfabtico (). valor valor valor valor (caracter) de a (en base 10) de (caracter) de a (en base 10) de + a + a b: + b: 177 b + 5: + b + 5: 182

En este manual evitaremos el uso de variables de tipo char con operadores aritmticos: cuando necesitemos una variable entera de rango pequeo la declararemos de tipo short. Las variables de tipo char las utilizaremos nicamente en su dominio de caracteres y las operaciones que con ellos podamos realizar: se ver ms adelante.

7.14.

Escriba un programa que calcule la superficie y el volumen de una esfera cuyo radio es introducido por teclado.

S 4 r2

V 4 3 r3

Recuerde que, en C, la expresin 4 3 es idnticamente igual a 1. Piense la manera de expresar, en C, el valor resultante de dividir 4 entre 3.

7.15.

El nmero ureo ( ) es aquel que verifica la propiedad de que al elevarlo al cuadrado se obtiene el mismo valor que al sumarle 1. Haga un programa que calcule y muestre por pantalla el nmero ureo.

168

Captulo 7. Tipos de dato y variables en C.

#include <stdio.h> #include <math.h> int main(void) { double aureo; printf("Nmero AUREO: tal que x + 1 = x * x.\n"); // Clculo del nmero Aureo // x^2 = x + 1 ==> x^2 - x - 1 = 0 ==> x = (1 + sqrt(5)) / 2. aureo = (1 + sqrt(5)) / 2; printf("El nmero AUREO es %10.8lf\n",aureo); printf("aureo + 1 ........ %10.8lf\n",aureo + 1); printf("aureo * aureo .... %10.8lf\n", aureo * aureo); return 0; } La funcin sqrt est definida en la biblioteca math.h. Calcula el valor de la raz cuadrada de un nmero. Espera como parmetro una variable de tipo double, y devuelve el valor en este formato o tipo de dato. El ejercicio es muy sencillo. La nica complicacin (si se le puede llamar complicacin a esta trivialidad) es saber cmo se calcula el nmero ureo a partir de la definicin aportada. Muchas veces el problema de la programacin no est en el lenguaje, sino en saber expresar una solucin viable de nuestro problema.

7.16.

Lea el siguiente cdigo y muestre la salida que ofrecer por pantalla:

#include <stdio.h> int main(void) { short a = 0xFFF4, b = 12, c = a == -b ? 1: 0; printf("%hd \t %hd \t %hd", a, b, c); return 0;

169

Informtica Aplicada. Programacin en Lenguaje C.

SOLUCIN: -12

12

1. Explquelo!

7.17.

Muestre la salida que, por pantalla, ofrecer el siguiente cdigo:

#include <stdio.h> int main(void) { short a = 0xFFFF; unsigned short b = 0xFFFF; printf("%s", a == b ? "IGUALES" : "DISTINTOS"); return 0; } La solucin es : DISTINTOS. Por qu?: La variable a es de tipo entero corto con signo; su rango o dominio de valores es el intervalo [-32.768 .. +32.767]. Al asignarle el literal hexadecimal 0xFFFF se le da un valor negativo (su bit ms significativo es un 1), igual a 1. La variable b es de tipo entero corto sin signo; su rango o dominio de valores es el intervalo [0 .. +65.535]. Al asignarle el literal hexadecimal 0xFFFF se le da un valor positivo (este tipo de dato no admite valores menores que cero), igual a +65535. Desde luego ambos valores son diferentes, y as los considera el programa que aqu se recoge. Qu ocurrira si el enunciado hubiera sido otro con sta de ahora declaracin de variables: short a = 0xFFFF; long b = 0xFFFF;

170

Captulo 7. Tipos de dato y variables en C.

De nuevo el resultado hubiera sido DISTINTOS, porque ahora el valor de la variable b es positivo puesto que la variable b tiene 32 bits y el literal slo asigna valor a los 16 menos significativos: el valor de b comienza con el bit cero.

7.18.

Escriba un programa que solicite un valor entero no negativo menor que 10000 y muestre en pantalla y separados el dgito de los millares, de las centenas, de las decenas y de las unidades.

El programa debe solicitar un entero entre 0 y 9999. Y mostrar por pantalla, mediante unas sencillas operaciones, los millares, centenas, decenas y unidades del entero. Un posible cdigo del problema podra ser el siguiente: #include <stdio.h> int main(void) { unsigned long a; printf("Valor de a ... "); scanf("%lu", &a);

printf("\nMillares: %hd", a / 1000); a = a % 1000; printf("\nCentenas: %hd", a / 100); a = a % 100; printf("\nDecenas: %hd", a / 10); a = a % 10; printf("\nUnidades: %hd", a); return 0; }

7.19.

Escriba un programa que calcule a qu distancia caer un proyectil lanzado desde un can. El programa recibe desde teclado el ngulo inicial de salida del proyectil ( ) y su velocidad inicial ( ). Se supone que el can est en el suelo y

171

Informtica Aplicada. Programacin en Lenguaje C.

que el proyectil cae ms all, pero a la misma altura: es decir, que el suelo es horizontal. Tenga en cuenta las siguientes ecuaciones que definen el comportamiento del sistema descrito: (1) (3) (2) (4) .

Este problema es muy sencillo. Pero hay que saber resolverlo. Como en muchos de los problemas a los que se enfrenta un programador, la miga o el reto no est en el cdigo a escribir, sino en conocer un camino que resuelva el problema. Por eso, antes de comenzar siquiera a pensar en un algoritmo, debemos resolver el problema desde un punto de vista matemtico. De las cuatro expresiones presentadas, la (1) no nos sirve para nada. Averiguamos la distancia calculando, mediante la expresin (2), el tiempo para que (cuando llega a la mxima altura) y multiplicando por dos; o calculando, mediante la expresin (4), el tiempo para que y vuelva a ser igual a cero. Una vez calculado el tiempo, se sustituye ste en la expresin (3) y obtenemos el alcance. Y para decidir que eso es as, no hay que aprender a programar: es una decisin basada en los conocimientos de cinemtica. Tomamos el segundo camino indicado. Hemos quedado que suponemos que el can est a la misma altura que el suelo donde caer el proyectil. En ese momento tendremos que llegamos a que : Instante inicial cuando se dispara el proyectil. : Instante en que cae el proyectil. . Buscamos en qu instantes ocurre esta igualdad. Para ello, resolviendo la ecuacin (4)

172

Captulo 7. Tipos de dato y variables en C.

Desde luego, en el instante inicial el proyectil est a la altura del suelo. A nosotros nos interesa el segundo momento en que vuelve a estar en el suelo. Y en ese instante, sustituyendo el valor de (3), calcular el valor de la : . As, con esta expresin, el algoritmo es muy sencillo: 1. [Entrada de datos]: Leer y . 2. [Clculos] 2.1. . 2.2. . 3. [Mostrar resultados]: Mostrar . 4. Fin. Ahora que ya sabemos cmo se resuelve, slo queda el trabajo de escribir un programa en C que lo implemente el algoritmo. Lo primero que deberemos decidir es el tipo de las variables para la velocidad inicial, para el ngulo y para la distancia calculada. Parece obvio que esas variables debern ser de coma flotante. Aqu se presenta una solucin con el tipo double. #include <stdio.h> #include <math.h> int main(void) { double v0, alfa; double x; const double g = 9.8; printf("Velocidad inicial ... "); scanf("%lf", &v0); printf("ngulo ... "); scanf("%lf", &alfa); alfa *= M_PI / 180; x = 2 * v0 * v0 * sin(alfa) * cos(alfa); x = x / g; printf("Distancia ... %lf\n", x); return 0; } en la expresin

173

Informtica Aplicada. Programacin en Lenguaje C.

El valor de la aceleracin de la gravedad, la constante g, la hemos definido como una constante. Se podra haber utilizado una directiva define, o simplemente el literal 9.8. Las funciones de clculo del seno y del coseno estn definidas en el archivo de cabecera math.h: por eso lo hemos incluido al principio de nuestro programa. Si se consulta cualquier ayuda sobre estas funciones, se comprueba que el devuelven un valor double, y que reciben tambin un valor double, donde se recoge el valor del ngulo expresado en radianes. Como se supone que el usuario introducir el ngulo en grados, debemos, antes de invocar a esas funciones, convertir esos ngulos en sus expresiones en radianes. El valor de la constante pi est definido en el archivo math.h, que lo recoge con una directiva define con la etiqueta :M_PI.

174

CAPTULO 8
FUNCIONES DE ENTRADA Y SALIDA POR CONSOLA.
Hasta el momento, hemos presentado las sentencias de creacin y declaracin de variables. Tambin hemos visto multitud de operaciones que se pueden realizar con las diferentes variables y literales. Pero an no sabemos cmo mostrar un resultado por pantalla. Y tampoco hemos aprendido todava a introducir informacin, para un programa en ejecucin, desde el teclado. El objetivo de este captulo es iniciar en la comunicacin entre el programa y el usuario. Lograr que el valor de una variable almacenada de un programa sea mostrado por pantalla sera una tarea compleja si no fuese porque ya ANSI C ofrece funciones que realizan esta tarea. Y lo mismo ocurre cuando el programador quiere que sea el usuario quien teclee una entrada durante la ejecucin del programa. Quiz a usted le parezca

Informtica Aplicada. Programacin en Lenguaje C.

trivial el hecho de que, al pulsar una tecla del teclado aparezca un carcter concreto y correcto en la pantalla. Quiz eso es lo que usted esperaba que hiciera, y lleva toda su vida acostumbrado a que as se cumpla. Pero que usted est acostumbrado a verlo no quiere decir que sea trivial. De hecho, usted posiblemente no sepa cmo ocurre. Estas funciones, de entrada y salida estndar de datos por consola, estn declaradas en un archivo de cabecera llamado stdio.h. Siempre que deseemos usar estas funciones deberemos aadir, al principio del cdigo de nuestro programa, la directriz #include <stdio.h>.

Salida de datos. La funcin printf.


El prototipo de la funcin es el siguiente: int printf(const char *cadena_control[, argumento, ...]); Qu es un prototipo de una funcin es cuestin que habr que explicar en otro captulo. Sucintamente, diremos que el prototipo indica el modo de empleo de la funcin: su interfaz: qu tipo de dato devuelve y qu valores espera recibir cuando se hace uso de ella. El prototipo nos sirve para ver cmo se emplea esta funcin. La funcin printf devuelve un valor entero. Se dice que es de tipo int. La funcin printf devuelve un entero que indica el nmero de bytes que ha impreso en pantalla. Si, por la causa que sea, la funcin no se ejecuta correctamente, en lugar de ese valor entero lo que devuelve es un valor que significa error (por ejemplo un valor negativo). No descendemos a ms detalles. La funcin, como toda funcin, lleva despus del nombre un par de parntesis. Entre ellos va redactado el texto que deseamos que quede impreso en la pantalla. La cadena_control indica el texto que debe ser impreso, con unas especificaciones que indican el formato de esa impresin; es una cadena de caracteres recogidos entre comillas, que

176

Captulo 8. Funciones de entrada y salida por consola.

indica el texto que se ha de mostrar por pantalla. A lo largo de este captulo aprenderemos a crear esas cadenas de control que especifican la salida y el formato que ha de mostrar la funcin printf. Para comenzar a practicar, empezaremos por escribir en el editor de C el siguiente cdigo. Es muy recomendable que a la hora de estudiar cualquier lenguaje de programacin, y ahora en concreto el lenguaje C, se trabaje delante de un ordenador que tenga un editor y un compilador de cdigo: #include <stdio.h> int main(void) { printf(Texto a mostrar en pantalla); return 0; } Que ofrecer la siguiente salida por pantalla Texto a mostrar en pantalla Y as, cualquier texto que se escriba entre las comillas aparecer en pantalla. Si introducimos ahora otra instruccin con la funcin printf a

continuacin y debajo de la otra, por ejemplo printf(Otro texto); Entonces lo que tendremos en pantalla ser Texto a mostrar en pantallaOtro texto Y es que la funcin printf continua escribiendo donde se qued la vez anterior. Muchas veces nos va a interesar introducir, en nuestra cadena de caracteres que queremos imprimir por pantalla, algn carcter de, por ejemplo, salto de lnea. Pero si tecleamos la tecla intro en el editor de C lo que hace el cursor en el editor es cambiar de lnea y eso que no queda reflejado luego en el texto que muestra el programa en tiempo de ejecucin.

177

Informtica Aplicada. Programacin en Lenguaje C.

Para poder escribir este carcter de salto de lnea, y otros que llamamos caracteres de control, se escribe, en el lugar de la cadena donde queremos que se imprima ese carcter especial, una barra invertida (\) seguida de una letra. Cul letra es la que se debe poner depender de qu carcter especial se desea introducir. Esos caracteres de control son caracteres no imprimibles, o caracteres que tienen ya un significado especial en la funcin printf. Por ejemplo, el cdigo anterior quedara mejor de la siguiente forma: #include <stdio.h> int main(void) { printf(Texto a mostrar en pantalla\n); printf(Otro texto.); return 0; } que ofrecer la siguiente salida por consola: Texto a mostrar en pantalla Otro texto Ya que al final de la cadena del primer printf hemos introducido un carcter de control salto de lnea: \n significa, dentro de la funcin printf, salto de lnea. Las dems letras con significado para un carcter de control en esta funcin vienen recogidas en la tabla 8.1. Muchas pruebas se pueden hacer ya en el editor de C, para compilar y ver la ejecucin que resulta. Gracias a la ltima opcin de la tabla 8.1. es posible imprimir todos los caracteres ASCII y los tres inmediatamente anteriores sirven para imprimir caracteres que tienen un significado preciso dentro de la cadena de la funcin printf. Gracias a ellos podemos imprimir, por ejemplo, un carcter de comillas dobles evitando que la funcin printf interprete ese carcter como final de la cadena que se debe imprimir. El siguiente paso, una vez visto cmo imprimir texto prefijado, es imprimir en consola el valor de una variable de nuestro programa.

178

Captulo 8. Funciones de entrada y salida por consola.

\a \v \0 \n \t \b \r \f \ \ \\ \xdd

Carcter sonido. Emite un pitido breve. Tabulador vertical. Carcter nulo. Nueva lnea. Tabulador horizontal. Retroceder un carcter. Retorno de carro. Salto de pgina. Imprime la comilla simple. Imprime la comilla doble. Imprime la barra invertida \. dd es el cdigo ASCII, en hexadecimal, del carcter que se desea imprimir.

Tabla 8.1.: Caracteres de control en la cadena de la funcin printf.

Cuando en un texto a imprimir se desea intercalar el valor de una variable, en la posicin donde debera ir ese valor se coloca el carcter % seguido de algunos caracteres. Segn los caracteres que se introduzcan, se imprimir un valor de un tipo de dato concreto, con un formato de presentacin determinado. Ese carcter % y esos caracteres que le sigan son los especificadores de formato. Al final de la cadena, despus de las comillas de cierre, se coloca una coma y el nombre de la variable que se desea imprimir. Por ejemplo, vea el siguiente cdigo: #include <stdio.h> int main(void) { short int a = 5 , b = 10 , c; c = a + b++; printf(Ahora c vale %hd \n,c); printf(y b vale ahora %hd ,b); return 0; } Que ofrece la siguiente salida por pantalla:

179

Informtica Aplicada. Programacin en Lenguaje C.

Ahora c vale 15 y b vale ahora 11 Una cadena de texto de la funcin printf puede tener tantos

especificadores de formato como se desee. Tantos como valores de variables queramos imprimir por pantalla. Al final de la cadena, y despus de una coma, se incluyen tantas variables, separadas tambin por comas, como especificadores de formato se hayan incluido en la cadena de texto. Cada grupo de caracteres encabezado por % en el primer argumento de la funcin (la cadena de control) est asociado con el correspondiente segundo, tercero, etc. argumento de esa funcin. Debe existir una correspondencia biunvoca entre el nmero de especificadores de formato y el nmero de variables que se recogen despus de la cadena de control; de lo contrario se obtendrn resultados imprevisibles y sin sentido. El especificador de formato instruye a la funcin sobre la forma en que deben ir impresos cada uno de los valores de las variables que deseamos que se muestren por pantalla. Los especificadores tienen la forma: %[flags][ancho campo][.precisin][F/N/h/l/L] type Veamos los diferentes componentes del especificador de formato: type: Es el nico argumento necesario. Consiste en una letra que indica el tipo de dato a que corresponde al valor que se desea imprimir en esa posicin. En la tabla 8.2. se recogen todos los valores que definen tipos de dato. Esta tabla est accesible en las ayudas de editores y compiladores de C. El significado de estas letras es bastante intuitivo: si queremos imprimir un valor tipo char, pondremos %c. Si queremos imprimir un valor de tipo int pondremos %i. Si ese entero es sin signo (unsigned int), entonces pondremos %u. La funcin printf permite expresar los enteros en diferentes bases. Si no especificamos nada, y ponemos %i, entonces imprime en base 10; si queremos que el valor numrico vaya expresado en base octal pondremos %o; si queremos

180

Captulo 8. Funciones de entrada y salida por consola.

que en hexadecimal, %x %X: en minscula o mayscula dependiendo de cmo queramos que aparezcan (en minscula o mayscula) los dgitos hexadecimales A, B, C, D, E, y F. Si queremos indicarle expresamente que los valores aparezcan en base decimal, entonces pondremos %d: como se ve, poner %d y poner %i es equivalente. Por ltimo, si queremos imprimir un valor de tipo coma flotante (float, double, o long double), pondremos %f; tambin puede poner %G %E si desea ver esos valores en notacin cientfica (signo, exponente, mantisa) Los dems valores presentados en la tabla 8.2. no se los puedo explicar todava: no ha visto an qu son los punteros ni las cadenas de caracteres. Habr que esperar. [F / N / h / l / L]: Estas cinco letras son modificadores de tipo y preceden a las letras que indican el tipo de dato que se debe mostrar por pantalla. La letra h es el modificador short para valores enteros. Se puede poner con las letras de tipo de dato i, u, d, o, x, X. No tendra sentido poner, por ejemplo, %hc, %hf, porque no existe el tipo de dato short char, short float. La letra l tiene dos significados: es el modificador long para valores enteros. Y, precediendo a la letra f indica que all debe ir un valor de tipo double. Por lo tanto, podemos poner %li, %lu, %ld, para variables signed long; %lu para variables unsigned long; %lo, %lx, %lX para variables tanto signed long como unsigned long: al mostrar el valor en su codificacin binaria (el octal o el hexadecimal no son en realidad ms que formas cmodas de expresar binario) queremos ver sus ceros y unos: es tarea entonces nuestra saber interpretar si esos valores son, en cada caso, positivos o negativos. Si ponemos %lf, entonces es que vamos a imprimir el valor de una variable double. La letra L precediendo a la letra f indica que all debe ir un valor de tipo long double. No hay otro significado posible. Cualquier otra combinacin con la letra modificadora L ser un error.

181

Informtica Aplicada. Programacin en Lenguaje C.

%d %i %o %u %x %X %f %e %E %g %G %c %s %p %n %%

Entero con signo, en base decimal. Entero con signo, en base decimal. Entero (con o sin signo) codificado en base octal. Entero sin signo, en base decimal. Entero (con o sin signo) codificado en base hexadecimal, usando letras minsculas. Codificacin interna de los enteros. Entero (con o sin signo) codificado en base hexadecimal, usando letras maysculas. Codificacin interna de los enteros. Nmero real con signo. Nmero real con signo en formato cientfico, con el exponente e en minscula. Nmero real con signo en formato cientfico, con el exponente e en mayscula. Nmero real con signo, a elegir entre formato e f segn cul sea el tamao ms corto. Nmero real con signo, a elegir entre formato E f segn cul sea el tamao ms corto. Un carcter. El carcter cuyo ASCII corresponda con el valor a imprimir. Cadena de caracteres. Direccin de memoria. No lo explicamos aqu ahora. Si el carcter % no va seguido de nada, entonces se imprime el carcter sin ms.

Tabla 8.2.: Especificadores de tipo de dato en la funcin printf.

Con el estndar C99 se introdujeron los tipos de datos long long. Para indicar a la funcin printf que el valor a insertar es de ese tipo, se ponen las letras ll (dos veces la ele). Si el valor a mostrar es signed long long int entonces podremos %lli ( %llX, %llx, %llo, %lld). Si el valor a mostrar es unsigned long long int entonces podremos %llu ( %llX, %llx, %llo).

182

Captulo 8. Funciones de entrada y salida por consola.

El estndar C99 permite tambin trabajar con variables enteras de 1 byte: son las de tipo signed char. En ese caso, para indicar a la funcin printf que el valor a insertar es de ese tipo corto, se ponen las letras hh (dos veces la hache). Si el valor a mostrar es signed char entonces podremos %hhi ( %hhX, %hhx, %hho, %hhd).

[ancho campo][.precisin]: Estos dos indicadores opcionales deben ir antes de los indicadores del tipo de dato. Con el ancho de campo, el especificador de formato indica a la funcin printf la longitud mnima que debe ocupar la impresin del valor que all se debe mostrar. Para mostrar informacin por pantalla la funcin printf emplea un tipo de letra de paso fijo. Esto quiere decir que cada carcter impreso ocasiona el mismo desplazamiento del cursor hacia la derecha. Al decir que el ancho de campo indica la longitud mnima se quiere decir que este parmetro seala cuntos avances de cursor deben realizarse, como mnimo, al imprimir el valor. Por ejemplo, las instrucciones long int a printf(La printf(La printf(La = 123, b variable variable variable = a b c 4567, c = 135790; vale ... %6li.\n,a); vale ... %6li.\n,b); vale ... %6li.\n,c);

tiene la siguiente salida: La variable a vale ... 123. La variable b vale ... 4567. La variable c vale ... 135790. donde vemos que los tres valores impresos en lneas diferentes quedan alineados en sus unidades, decenas, centenas, etc. gracias a que todos esos valores se han impreso con un ancho de campo igual a 6: su impresin ha ocasionado tantos desplazamientos de cursos como indica el ancho de campo. Si la cadena o nmero es mayor que el ancho de campo indicado ignorar el formato y se emplean tantos pasos de cursor como sean necesarios para imprimir correctamente el valor.

183

Informtica Aplicada. Programacin en Lenguaje C.

Si se desea, es posible rellenar con ceros los huecos del avance de cursor. Para ellos se coloca un 0 antes del nmero que indica el ancho de campo La instruccin printf(La variable a vale ... %06li.\n,a); ofrece como salida la siguiente lnea en pantalla: La variable a vale ... 000123. El parmetro de precisin se emplea para valores con coma flotante. Indica el nmero de decimales que se deben mostrar. Indica cuntos dgitos no enteros se deben imprimir: las posiciones decimales. A ese valor le precede un punto. Si el nmero de decimales del dato almacenado en la variable es menor que la precisin sealada, entonces la funcin printf completa con ceros ese valor. Si el nmero de decimales del dato es mayor que el que se indica en el parmetro de precisin, entonces la funcin printf trunca el nmero. Por ejemplo, el cdigo double raiz_2 = printf("A. Raiz printf("B. Raiz printf("C. Raiz printf("D. Raiz printf("E. Raiz printf("F. Raiz printf("G. Raiz printf("H. Raiz printf("I. Raiz sqrt(2); de dos vale de dos vale de dos vale de dos vale de dos vale de dos vale de dos vale de dos vale de dos vale %lf\n",raiz_2); %12.1lf\n",raiz_2); %12.3lf\n",raiz_2); %12.5lf\n",raiz_2); %12.7lf\n",raiz_2); %12.9lf\n",raiz_2); %12.11lf\n",raiz_2); %5.7lf\n",raiz_2); %012.4lf\n",raiz_2);

que ofrece la siguiente salida por pantalla: A. B. C. D. E. F. G. H. I. Raiz Raiz Raiz Raiz Raiz Raiz Raiz Raiz Raiz de de de de de de de de de dos dos dos dos dos dos dos dos dos vale vale vale vale vale vale vale vale vale 1.414214 1.4 1.414 1.41421 1.4142136 1.414213562 1.41421356237 1.4142136 0000001.4142

184

Captulo 8. Funciones de entrada y salida por consola.

La funcin sqrt est declarada en el archivo de cabecera math.h. Esta funcin devuelve la raz cuadrada (en formato double) del valor (tambin double) que ha recibido como parmetro de entrada, entre parntesis. Por defecto, se toman seis decimales, sin formato alguno. Se ve en el ejemplo el truncamiento de decimales. En el caso G., la funcin printf hace caso omiso del ancho de campo pues se exige que muestre un valor que tiene un carcter para la parte entera, otro para el punto decimal y once para los decimales: en total 13 caracteres, y no 12 como seala en ancho de campo. y es que el punto decimal es un carcter ms dentro de la impresin por pantalla del valor. La funcin printf, en lnea H, no puede hacer caso a la indicacin del ancho de campo: no tiene sentido pretender que el nmero ocupe un total de 5 espacios y a la vez exigir que slo la parte decimal ocupe 7 espacios. [flags]: Son caracteres que introducen unas ltimas modificaciones en el modo en que se presenta el valor. Algunos de sus valores y significados son: carcter : el valor queda justificado hacia la izquierda. carcter +: el valor se escribe con signo, sea ste positivo o negativo. En ausencia de esta bandera, la funcin printf imprime el signo nicamente si es negativo. carcter en blanco: Si el valor numrico es positivo, deja un espacio en blanco. Si es negativo imprime el signo. Esto permite una mejor alineacin de los valores, haciendo coincidir unidades con unidades, decenas con decenas, centenas con centenas, etc. Por ejemplo: #include <stdio.h> int main(void) { short a = 2345; printf("%4hd\n", a); printf("%4hd\n", -a); return 0; } Cuya salida por pantalla es:

2345 -2345

185

Informtica Aplicada. Programacin en Lenguaje C.

#include <stdio.h> int main(void) { short a = 2345; printf("%+4hd\n", printf("%+4hd\n", return 0; } #include <stdio.h> int main(void) { short a = 2345; printf("% 4hd\n", printf("% 4hd\n", return 0; }

Cuya salida por pantalla es:

a); -a);

+2345 -2345
Cuya salida por pantalla es:

a); -a);

2345 -2345

Existen otras muchas funciones que muestran informacin por pantalla. Muchas de ellas estn definidas en el archivo de cabecera stdio.h. Con la ayuda a mano, es sencillo aprender a utilizar muchas de ellas.

Entrada de datos. La funcin scanf.


La funcin scanf de nuevo la encontramos declarada en el archivo de cabecera stdio.h. Permite la entrada de datos desde el teclado. La ejecucin del programa queda suspendida en espera de que el usuario introduzca un valor y pulse la tecla de validacin (intro). La ayuda de cualquier editor y compilador de C es suficiente para lograr hacer un buen uso de ella. Presentamos aqu unas nociones bsicas, suficientes para su uso ms habitual. Para la entrada de datos, al igual que ocurra con la salida, hay otras funciones vlidas que tambin pueden conocerse a travs de las ayudas de los distintos editores y compiladores de C. El prototipo de la funcin es: int scanf(const char *cadena_control[,direcciones,]);

186

Captulo 8. Funciones de entrada y salida por consola.

La funcin scanf puede leer del teclado tantas entradas como se le indiquen. De todas formas, se recomienda usar una funcin scanf para cada entrada distinta que se requiera. El valor que devuelve la funcin es el del nmero de entradas diferentes que ha recibido. Si la funcin ha sufrido algn error, entonces devuelve un valor que significa error (por ejemplo, un valor negativo). En la cadena de control se indica el tipo de dato del valor que se espera recibir por teclado. No hay que escribir texto alguno en la cadena de control de la funcin scanf: nicamente el especificador de formato.

%d %i %o %u %x %X %f %e %c %s %p %n

Entero con signo, en base decimal. Entero con signo, en base decimal. Entero codificado en base octal. Entero sin signo, en base decimal. Entero codificado en base hexadecimal, usando letras minsculas. Codificacin interna de los enteros. Entero codificado en base hexadecimal, usando letras maysculas. Codificacin interna de los enteros. Nmero real con signo. Nmero real con signo en formato cientfico, con el exponente e en minscula. Un carcter. El carcter cuyo ASCII corresponda con el valor a imprimir. Cadena de caracteres. Direccin de memoria. No lo explicamos aqu ahora.

Tabla 8.3.: Especificadores de tipo de dato en la funcin scanf.

El formato de este especificador es similar al presentado en la funcin printf: un carcter % seguido de una o dos letras que indican el tipo de dato que se espera. Luego, a continuacin de la cadena de control, y despus de una coma, se debe indicar dnde se debe almacenar ese

187

Informtica Aplicada. Programacin en Lenguaje C.

valor: la posicin de una variable que debe ser del mismo tipo que el indicado en el especificador. El comportamiento de la funcin scanf es imprevisible cuando no coinciden el tipo sealado en el especificador y el tipo de la variable; en ese caso, habitualmente aborta la ejecucin del programa. Las letras que indican el tipo de dato a recibir se recogen en la tabla 8.3. Los modificadores de tipo de dato son los mismos que para la funcin printf. La cadena de control tiene otras especificaciones, pero no las vamos a ver aqu. Se pueden obtener en la ayuda del compilador. Adems de la cadena de control, la funcin scanf requiere de otro parmetro: el lugar dnde se debe almacenar el valor introducido. La funcin scanf espera, como segundo parmetro, el lugar donde se aloja la variable, no el nombre. Espera la direccin de la variable. As est indicado en su prototipo. Para poder saber la direccin de una variable, C dispone de un operador unario: &. El operador direccin, prefijo a una variable, devuelve la direccin de memoria de esta variable. El olvido de este operador en la funcin scanf es frecuente en programadores noveles. Y de consecuencias desastrosas: siempre ocurre que el dato introducido no se almacena en la variable que desebamos, alguna vez producir alteraciones de otros valores y las ms de las veces llevar a la inestabilidad del sistema y se deber finalizar la ejecucin del programa.

Dificultades habituales con las entradas de caracteres.


Cuando la funcin scanf pretende tomar del teclado un valor de tipo char, con frecuencia aparecen problemas. Lo mismo ocurrir si utilizamos otras funciones estndares de stdio.h para la toma de valores

188

Captulo 8. Funciones de entrada y salida por consola.

de variables de tipo char o para cadenas de texto (cfr. Captulo 12 para este ltimo concepto de tipo de dato) Por ejemplo, copie el siguiente cdigo en un nuevo proyecto en su IDE y cree la siguiente funcin main: #include <stdio.h> int main(void) { char a, b, c, d; printf("Caracter printf("Caracter printf("Caracter printf("Caracter a b c d ... ... ... ... "); "); "); "); scanf(%c", scanf(%c", scanf(%c", scanf(%c", &a); &b); &c); &d);

printf("\n\nValores introducidos ... \n"); printf("Caracter a ... %c\n", a); printf("Caracter b ... %c\n", b); printf("Caracter c ... %c\n", c); printf("Caracter d ... %c\n", d); return 0; } Lo que ocurrir al ejecutar este cdigo ser que la ventana de ejecucin quedar a la espera de que usted introduzca el valor de un carcter para la variable a; y luego no le permita introducir el siguiente carcter para la variable b, sino que, directamente, se salte a la entrada de la variable c; finalmente tampoco le permitir la entrada del valor para la variable d. No se crea lo que le digo: hgalo: escriba el cdigo y ejecute. Lo que ver por pantalla, una vez terminada la ejecucin de la funcin, ser algo parecido a lo siguiente: Caracter a ... g Caracter b ... Caracter c ... q Caracter d ... Valores introducidos ... Caracter a ... g Caracter b ...

189

Informtica Aplicada. Programacin en Lenguaje C.

Caracter c ... q Caracter d ... Este fallo en la ejecucin es debido al modo en que se gestionan las entradas por teclado. Desde luego, al ejecutar nuestro programa, la funcin scanf no toma la entrada directamente del teclado: la funcin se alimenta de la informacin que est disponible en el buffer intermedio, que se comporta como un archivo de entrada. Explicar este comportamiento errneo exigira algo de teora que explicara la forma en que el ordenador y los programas tratan realmente los datos de entrada (en realidad caracteres introducidos al pulsar cada una de las teclas del teclado). Habra que explicar que el ordenador trata las entradas de teclado como si de un archivo se tratase. Slo que en lugar de leerlo sobre el disco duro, lo hace sobre uno que es estndar y que se llama stdin. Cuando el usuario pulsa una tecla alimenta con un nuevo dato ese archivo de entrada stdin. El ordenador dispone de espacio de memoria para poder gestionar esas entradas de teclado cuando estas se van acumulando. A esta memoria se la conoce como buffer. As, el ritmo de entrada de datos del usuario no deber ir necesariamente acompasado con el ritmo de lectura del ordenador o de sus programas. Cuando el usuario pulsa teclas, ingresa valores de carcter en el buffer. Cuando algn programa acude a la entrada (archivo stdin) para obtener informacin, toma la informacin del buffer y la interpreta de acuerdo a lo que el programa esperaba: cadena de caracteres, valor numrico entero o decimal, etc. No explicamos ms. Ahora usted debe aceptar que el funcionamiento de las que funciones ese de la biblioteca stdio.h no presuponen los el correcto que funcionamiento del archivo stdin y de su buffer. Y que, a veces, ocurre correcto funcionamiento ofrece resultados esperbamos. Por suerte, stdio.h ofrece otras funciones que ayudan a gestionar correctamente todas las entradas. Veamos algunas herramientas tiles.

190

Captulo 8. Funciones de entrada y salida por consola.

Primer consejo: cuando se utiliza la funcin scanf para entrada de caracteres (el caso que hemos presentado antes y donde hemos mostrado el fallo en el comportamiento del programa) est recomendado insertar, entre la primera de las comillas y el carcter %, un espacio en blanco. As le indicamos a la funcin scanf que ignore como entrada de carcter los caracteres en blanco, los tabuladores o los caracteres de nueva lnea. El cdigo antes propuesto funcionar correctamente si las lneas de entrada quedan de la siguiente forma: printf("Caracter printf("Caracter printf("Caracter printf("Caracter a b c d ... ... ... ... "); "); "); "); scanf(" scanf(" scanf(" scanf(" %c", %c", %c", %c", &a); &b); &c); &d);

Donde ha quedado insertado el espacio en blanco antes del carcter %. Pero existen otras funciones de entrada por teclado, definidas en stdio.h, que no admiten este truco que hemos presentado para la funcin scanf: las funciones getchar, o gets, por ejemplo. De la segunda nos ocuparemos en el captulo 12, al igual de volveremos entonces a la funcin scanf para entrada de cadenas enteras de caracteres y no slo de caracteres sueltos. En general, las dificultades en las entradas por teclado quedan resueltas con la funcin fflush. Esta funcin descarga el contenido del archivo que reciba como parmetro. Si, previo a la ejecucin de una funcin scanf para entrada de un valor tipo char o una cadena de texto, o previo a la ejecucin de las funciones getchar, o gets, u otras, insertamos la sentencia fflush(stdin); nunca tendremos problemas en la lectura de datos tipo carcter por teclado. El cdigo antes propuesto podra quedar de la siguiente forma: printf("Caracter printf("Caracter printf("Caracter printf("Caracter a b c d ... ... ... ... "); "); "); "); flush(stdin); flush(stdin); flush(stdin); flush(stdin); scanf( scanf( scanf( scanf( %c", %c", %c", %c", &a); &b); &c); &d);

191

Informtica Aplicada. Programacin en Lenguaje C.

Si ejecuta ahora el cdigo, comprobar que no se produce ningn error. Todo lo dicho sera tendra la misma explicacin con el siguiente cdigo: int main(void) { char a, b, c, d; printf("Caracter printf("Caracter printf("Caracter printf("Caracter a b c d ... ... ... ... "); "); "); "); a b c d = = = = getchar(); getchar(); getchar(); getchar();

printf("\n\nValores introducidos ... \n"); printf("Caracter a ... %c\n", a); printf("Caracter b ... %c\n", b); printf("Caracter c ... %c\n", c); printf("Caracter d ... %c\n", d); return 0; } Copie este cdigo y compruebe que, de nuevo, obtenemos un fallo en las entradas para las variables b y d. Arrglelo: printf("Caracter printf("Caracter printf("Caracter printf("Caracter a b c d ... ... ... ... "); "); "); "); fflush(stdin); fflush(stdin); fflush(stdin); fflush(stdin); a b c d = = = = getchar(); getchar(); getchar(); getchar();

Recapitulacin.
Hemos presentado el uso de las funciones printf y scanf, ambas declaradas en el archivo de cabecera stdio.h. Cuando queramos hacer uno de una de las dos funciones, o de ambas, deberemos indicarle al programa con la directiva de preprocesador #include <stdio.h>. El uso de ambas funciones se aprende en su uso habitual. Los ejercicios del captulo anterior pueden ayudar, ahora que ya las hemos presentado, a practicar con ellas.

192

Captulo 8. Funciones de entrada y salida por consola.

Ejercicios.

8.1.

Escribir un programa que muestre al cdigo ASCII de un carcter introducido por teclado.

#include <stdio.h> int main(void) { unsigned char ch; printf("Introduzca un carcter por teclado ... "); fflush(stdin); scanf( %c",&ch); printf("El carcter introducido ha sido %c\n",ch); printf("Su cdigo ASCII es el %hd", ch); return 0; } Primero mostramos el carcter introducido con el especificador de tipo %c: as muestra el carcter por pantalla. Y luego mostramos el mismo valor de la variable ch con el especificador %hd, es decir, como entero corto, y entonces nos muestra el valor numrico de ese carcter. Esta ltima lnea con funcin printf tambin podra haberse escrito as: printf("Su cdigo ASCII es el %hd", (short)ch); utilizando por tanto el operador forzar tipo.

8.2.

Lea el programa siguiente, e intente explicar la salida que ofrece por pantalla.

#include <stdio.h> int main(void)

193

Informtica Aplicada. Programacin en Lenguaje C.

{ signed long int sli; signed short int ssi; printf("Introduzca un valor negativo para sli ... "); scanf( %ld",&sli); printf("Introduzca un valor negativo para ssi ... "); scanf( %hd",&ssi); printf("El valor sli es %ld\n",sli); printf("El valor ssi es %ld\n\n",ssi); printf("El valor sli como \"%%lX\" es %lX\n",sli); printf("El valor ssi como \"%%hX\" es %hX\n\n",ssi); printf("El valor sli como \"%%lu\" es %lu\n",sli); printf("El valor ssi como \"%%hu\" es %hu\n\n",ssi); printf("El valor sli como \"%%hi\" es %hi\n",sli); printf("El valor ssi como \"%%li\" es %li\n\n",ssi); printf("El valor sli como \"%%hu\" es %hu\n",sli); printf("El valor ssi como \"%%lu\" es %lu\n\n",ssi); return 0; } La salida que ha obtenido su ejecucin es la siguiente: Introduzca un valor negativo para sli ... -23564715 Introduzca un valor negativo para ssi ... -8942 El valor sli es -23564715 El valor ssi es -8942 El valor sli como "%lX" es FE986E55 El valor ssi como "%hX" es DD12 El valor sli como "%lu" es 4271402581 El valor ssi como "%hu" es 56594 El valor sli como "%hi" es 28245 El valor ssi como "%li" es -8942 El valor sli como "%hu" es 28245 El valor ssi como "%lu" es 4294958354 Las dos primeras lneas no requieren explicacin alguna: recogen las entradas que se han introducido por teclado cuando se han ejecutado

194

Captulo 8. Funciones de entrada y salida por consola.

las instrucciones de la funcin scanf: el primero (sli) es long int, y se muestra con el especificador de formato %ld %li; el segundo (ssi) es short int, y se muestra con el especificador de formato %hd %hi. Las siguientes lneas de salida son: El valor sli como "%lX" es FE986E55 El valor ssi como "%hX" es DD12 Que muestran los nmeros tal y como los tiene codificados el ordenador: al ser enteros con signo, y ser negativos, codifica el bit ms significativo (el bit 31 en el caso de sli, el bit 15 en el caso de ssi) a uno porque es el bit del signo; y codifica el resto de los bits (desde el bit 30 al bit 0 en el caso de sli, desde el bit 14 hasta el bit 0 en el caso de ssi) como el complemento a la base del valor absoluto del nmero codificado. El nmero Y el nmero negativo, de la forma Las dos siguientes lneas son: El valor sli como "%lu" es 4271402581 El valor ssi como "%hu" es 56594 Muestra el contenido de la variable sli que considera ahora como entero largo sin signo. Y por tanto toma esos 32 bits, que ya no los considera como un bit de signo y 31 de complemento a la base del nmero negativo, sino 32 bits de . Y muestra el contenido de la variable ssi que considera ahora como entero corto sin signo. Y por tanto toma esos 16 bits, que ya no los considera como un bit de signo y 15 de complemento a la base del nmero negativo, sino 16 bits de valor positivo codificado en binario: . Las dos siguientes lneas son: valor positivo codificado en binario: . se codifica, como nmero negativo (dgito . se codifica, como nmero

15 a 1 y resto el valor en su complemento a la base), de la forma

195

Informtica Aplicada. Programacin en Lenguaje C.

El valor sli como "%hi" es 28245 El valor ssi como "%li" es -8942 Al mostrar el valor de sli como variable corta, ha truncado los 16 bits ms significativos: en lugar de mostrar expresado en base 10, del cdigo , muestra nicamente el valor en base 10 del cdigo : ahora lo interpreta como un entero positivo (comienza con el dgito hexadecimal 6: 0110) cuyo valor expresado en base decimal es . Respecto al valor de la variable ssi, al ser esta corta y promocionarla a entero largo, simplemente aade a su izquierda diecisis ceros si el entero codificado es positivo; y diecisis unos si el entero codificado es negativo: as logra recodificar el valor a un tipo de dato de mayor rango, sin variar su significado: imprime el valor nuevo, el valor introducido por teclado: Las dos ltimas lneas son: El valor sli como "%hu" es 28245 El valor ssi como "%lu" es 4294958354 La primera de ellas considera la variable sli como una variable corta de 16 bits. Por tanto lo que hace es tomar los 16 bits menos significativos de la variable de 32 bits y los interpreta como entero corto sin signo: . El comportamiento es igual que en el caso anterior: entero truncado, interpretado como positivo. En la segunda lnea muestra el valor numrico que, expresado en base hexadecimal, es igual a . Como se explic antes, ha aadido 16 unos a la izquierda. Pero ahora, al tener que interpretarlos como entero sin signo, lo que hace es mostrar ese valor en su base 10: no toma en consideracin ninguna referencia al signo, ni interpreta el valor codificado como complemento a la base: simplemente muestra, en base 10, el valor numrico que en base hexadecimal es es decir, . , . , que es, de

196

Captulo 8. Funciones de entrada y salida por consola.

8.3.

Escriba el siguiente programa y compruebe cmo es la salida que ofrece por pantalla.

#include <stdio.h> #include <math.h> int main(void) { double a = M_PI; printf(" 1. printf(" 2. printf(" 3. printf(" 4. printf(" 5. printf(" 6. printf(" 7. printf(" 8. printf(" 9. printf("10. printf("11. printf("12. printf("13. printf("14. printf("15. return 0; } La salida que ofrece por pantalla es la siguiente: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. El El El El El El El El El El El El El El El valor valor valor valor valor valor valor valor valor valor valor valor valor valor valor de de de de de de de de de de de de de de de Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi es es es es es es es es es es es es es es es ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... 3.1 3.14 3.142 3.1416 3.14159 3.141593 3.1415927 3.14159265 3.141592654 3.1415926536 3.14159265359 3.141592653590 3.1415926535898 3.14159265358979 3.141592653589793 El El El El El El El El El El El El El El El valor valor valor valor valor valor valor valor valor valor valor valor valor valor valor de de de de de de de de de de de de de de de Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi Pi es es es es es es es es es es es es es es es ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... %20.1lf\n",a); %20.2lf\n",a); %20.3lf\n",a); %20.4lf\n",a); %20.5lf\n",a); %20.6lf\n",a); %20.7lf\n",a); %20.8lf\n",a); %20.9lf\n",a); %20.10lf\n",a); %20.11lf\n",a); %20.12lf\n",a); %20.13lf\n",a); %20.14lf\n",a); %20.15lf\n",a);

197

Informtica Aplicada. Programacin en Lenguaje C.

Donde hemos cambiado los espacios en blanco por puntos en la parte de la impresin de los nmeros. Y donde el archivo de biblioteca math.h contiene el valor del nmero pi, en la constante o identificador M_PI. Efectivamente, emplea 20 espacios de carcter de pantalla para mostrar cada uno de los nmeros. Y cambia la posicin de la coma decimal, pues cada lnea exigimos a la funcin printf que muestre un decimal ms que en la lnea anterior.

8.4.

Escriba un programa que genere 20 nmeros de coma flotante de forma aleatoria, y los muestre, uno debajo de otro, alineados por la coma decimal.

#include <stdio.h> #include <stdlib.h> #include <time.h> int main(void) { double a; short i; srand(time(NULL)); for(i = 1 ; i <= 20 ; i++) { a = rand() * 100.0 / rand(); printf("%2hd. %10.5lf\n", i, a); srand(rand()); } return 0; } No se preocupe ahora con estas funciones que usted no conoce: srand(), rand(), y time(). Cntrese simplemente en la funcin printf. Tampoco hemos llegado al captulo de las estructuras de control, pero podemos entender por ahora, viendo el cdigo, que este programa realiza veinte veces la operacin de generar dos enteros de forma

198

Captulo 8. Funciones de entrada y salida por consola.

aleatoria (la funcin rand() genera un entero aleatorio), dividirlos en cociente de coma flotante (al multiplicar el primero por 100.0 ya hemos logrado que ese valor se comporte, en la ALU, como double; multiplicamos por 100.0 para lograr que, en general, los aleatorios sean mayores que la unidad) y mostramos los distintos resultados, uno debajo del otro, con el especificador de tipo de dato %10.5lf. As, si los nmeros generados son menores que 100.000 quedarn en lnea, como se ve en la salida que se muestra a continuacin: 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 57.21568 62.41973 147.16501 120.04998 215.02813 52.82802 260.75406 721.83456 9.85598 150.42073 7.11266 78.85494 73.41685 196.21048 88.43795 192.40674 315.92087 50.11689 7.16849 187.26519

Donde la coma decimal queda ubicada, en todos los nmeros, en la misma columna. Y as tambin las unidades, decenas, y decimales.

199

Informtica Aplicada. Programacin en Lenguaje C.

200

Captulo 8. Funciones de entrada y salida por consola.

ANEXO: Ficha resumen de la funcin printf printf <stdio.h>

int printf (formato [ , argumento , ...] ); Imprime los datos formateados a la salida estndar segn especifica el argumento formato. formato Es una cadena de texto que contiene el texto que se va a imprimir. Opcionalmente puede contener etiquetas que se caracterizan por ir precedidas del carcter %. Estas etiquetas sern luego sustituidas cuando se ejecute la funcin por un valor especificado en los argumentos. El nmero de etiquetas de formato debe ser el mismo que el de los argumentos que se indiquen. Cada etiqueta indica la ubicacin donde se debe insertar un valor en la cadena de texto, y el formato en que se debe imprimir ese valor. Por cada etiqueta debe haber un argumento. El formato de etiquetas sigue el siguiente prototipo: %[flags][ancho][.precisin][modificadores]tipo donde tipo es imprescindible.

tipo c doi u o x X f e

Salida Carcter Decimal entero con signo Decimal entero sin signo Octal con signo Entero hexadecimal sin signo Entero hexadecimal sin signo (letras maysculas) Decimal en punto flotante Notacin cientfica (mantisa/exponente) con

Ejemplo a 392 7235 610 7fa 7FA 392.65 3.9265e2

201

Informtica Aplicada. Programacin en Lenguaje C.

el carcter e E g G s p Notacin cientfica (mantisa/exponente) con el carcter E Usa el que sea ms corto entre %e y %f Usa el que sea ms corto entre %E y %f Cadena de caracteres Direccin apuntada por el argumento 3.9265E2 392.65 392.65 sample B800:0000

El resto de parmetros, esto es flags, ancho, .precisin y modificadores son opcionales y siguen el siguiente formato:

modificador

significado argumento se interpreta como un short int. argumento se interpreta como un signed char. argumento se interpreta como un long int cuando precede a un entero (d, i, o, u, x, X) o double si precede a un tipo de dato de coma flotante (f, g, G, e, E). argumento se interpreta como un long long int y debe ir precedido de una de las siguientes letras: d, i, o, u, x, X. argumento se interpreta como un long double si precede a un tipo de dato de coma flotante (f, g, G, e, E). significado Especifica el nmero mnimo de caracteres que se imprimen. Si el valor que se imprime es menor que este num entonces el resultado es completado con ceros. El valor nunca es truncado incluso si es mayor. El ancho no se especifica en el formato de la cadena sino que se indica en el valor entero que precede al argumento que tiene que formatearse. Para los tipos f, e, E, g, G: indica el nmero de dgitos que se imprimen despus del punto decimal (por defecto es 6). Para el tipo s: indica el nmero mximo de caracteres que se imprimen. (por defecto imprime hasta encontrar el primer carcter null).

h hh l

ll

ancho num

.precisin significado .num

202

Captulo 8. Funciones de entrada y salida por consola.

flags -

significado Alineacin a la izquierda con el ancho de campo dado (por defecto alinea a la derecha). Este flag slo tiene sentido cuando se especifica el ancho de campo. Obliga a anteponer un signo al resultado (+ o -) si el tipo es con signo. (por defecto slo el signo menos - se imprime). Si el argumento es un valor positivo con signo, se inserta un blanco antes del nmero. Coloca tantos ceros a la izquierda del nmero como sean necesarios para completar el ancho de campo especificado. Usado con los tipos o, x o X el valor es precedido con 0, 0x o 0X respectivamente si no son cero. Usado con e, E o f obliga a que el valor de salida contenga el punto decimal incluso aunque slo sigan ceros. Usado con g o G el resultado es el mismo que con e o E pero los ceros sobrantes no se eliminan.

blanco 0

argumento(s) Parmetro(s) opcional(es) que contiene(n) los datos que se insertarn en el lugar de los % etiquetas especificados en los parmetros del formato. Debe haber el mismo nmero de parmetros que de etiquetas de formato. Valor de retorno de printf. Si tiene xito representa el nmero total de caracteres impresos. Si hay un error, se devuelve un nmero negativo. Ejemplo. /* printf: algunos ejemplos de formato*/ #include <stdio.h> int main(void) { printf("Caracteres: %c %c \n", 'a', 65); printf("Decimales: %d %ld\n", 1977, 650000); printf("Precedidos de blancos: %10d \n", 1977); printf("Precedidos de ceros: %010d \n", 1977); printf("Formato: %d %x %o %#x %#o\n",100,100,100,100,100);

203

Informtica Aplicada. Programacin en Lenguaje C.

printf("float: %4.2f %+.0e %E\n", 3.1416, 3.1416, 3.1416); printf("Ancho: %*d \n", 5, 10); printf("%s \n", "Mi mam me mima"); return 0; } Y la salida: Caracteres: a A Decimales: 1977 650000 Precedidos con blancos: 1977 Precedidos con ceros: 0000001977 Formato: 100 64 144 0x64 0144 float: 3.14 +3e+000 3.141600E+000 Ancho: 10 Mi mam me mima

204

CAPTULO 9
ESTRUCTURAS DE CONTROL I: ESTRUCTURAS DE SELECCIN O SENTENCIAS CONDICIONADAS.
El lenguaje C pertenece a la familia de lenguajes del paradigma de la programacin estructurada. En este captulo y en el siguiente quedan recogidas las reglas de la programacin estructurada y, en concreto, las reglas sintcticas que se exige en el uso del lenguaje C para el diseo de esas estructuras. El objetivo de este captulo es mostrar cmo crear estructuras condicionales. Una estructura condicional permite al programa decidir, en funcin de una condicin expresada a travs de variables y literales y funciones y operadores, si ejecutar una determinada sentencia o grupo de sentencias, o ejecutar otras sentencias alternativas, o ninguna. Veremos tambin una estructura especial que permite seleccionar un camino de ejecucin de entre varios establecidos. Y tambin se presenta

Informtica Aplicada. Programacin en Lenguaje C.

un nuevo operador, que permite seleccionar, entre dos expresiones posibles, de acuerdo con una condicin previa que se evala como verdadera o como falsa.

Introduccin a las estructuras de control.


Ya hemos visto cules son las reglas bsicas de la programacin estructurada. Conviene recordarlas, al inicio de este captulo: 1. Todo programa consiste en una serie de acciones o sentencias que se ejecutan en secuencia, una detrs de otra. 2. Cualquier accin puede ser sustituida por dos o ms acciones en secuencia. Esta regla se conoce como la de apilamiento. 3. Cualquier accin puede ser sustituida por cualquier estructura de control; y slo se consideran tres estructuras de control: la secuencia, la seleccin y la repeticin. Esta regla se conoce como regla de anidamiento. Todas las estructuras de control de la programacin estructurada tienen un solo punto de entrada y un solo punto de salida. 4. Las reglas de apilamiento y de anidamiento pueden aplicarse tantas veces como se desee y en cualquier orden. Ya hemos visto cmo se crea una sentencia: con un punto y coma precedido de una expresin que puede ser una asignacin, la llamada a una funcin, una declaracin de una variable, etc. O, si la sentencia es compuesta, agrupando entonces varias sentencias simples en un bloque encerrado por llaves. Los programas discurren, de instruccin a instruccin, una detrs de

Instruccin 1

Instruccin 2

Instruccin N

2 Figura9.1.: Esquema de ordenacin secuencial de sentencias.

206

Captulo 9. Estructuras de control I: Estructuras de seleccin.

otra, en una ordenacin secuencial y nunca se ejecutan dos al mismo tiempo, como queda representado en la figura 9.1. Pero un lenguaje de programacin no slo ha de poder ejecutar las instrucciones en orden secuencial: es necesaria la capacidad para modificar ese orden de ejecucin. Para ello estn las estructuras de control. Al acabar este captulo y el siguiente, una vez conocidas todas las estructuras de control, las posibilidades de resolver diferentes problemas mediante el lenguaje de programacin C se habrn multiplicado enormemente. A lo largo del captulo iremos viendo algunos ejemplos. Es conveniente pararse en cada uno: comprender el cdigo que se propone en el manual, o lograr resolver aquellos que se dejan propuestos. En algunos casos ofreceremos el cdigo en C; en otros dejaremos apuntado el modo de resolver el problema ofreciendo el pseudocdigo del algoritmo o el flujograma.

Transferencia de control condicionada.


La regla 3 de la programacin estructurada habla de tres estructuras de control: la secuencia, la seleccin y la repeticin. Nada nuevo hay ahora que decir sobre la secuencia, que vendra esquematizada en la figura 9.1. En la figura 9.2. se esquematizan diferentes posibles estructuras de seleccin; y en la figura 9.3. las dos estructuras bsicas de repeticin que veremos detalladamente en el captulo 10. Las dos formas que rompen el orden secuencial de ejecucin de sentencias son: 1. Instruccin condicionada: Se evala una condicin y si se cumple se transfiere el control a una nueva direccin indicada por la instruccin.

207

Informtica Aplicada. Programacin en Lenguaje C.

No

Condicin

S
Instruccin

No
Instruccin

Condicin

S
Instruccin

Instruccin Bifurcacin abierta

Instruccin Bifurcacin cerrada

Figura 9.2.: Estructuras de seleccin.

No

Condicin

S No
Instruccin

Instruccin

Instruccin

Instruccin

Condicin

Estructura while

Estructura do - while

Figura 9.3.: Estructuras de repeticin. 2. Instruccin incondicionada. Se realiza la transferencia a una nueva direccin sin evaluar ninguna condicin (por ejemplo, llamada a una funcin). En ambos casos la transferencia del control se puede realizar con o sin retorno: en el caso de que exista retorno, despus de ejecutar el bloque de instrucciones de la nueva direccin se retorna a la direccin que sigue o sucede a la que ha realizado el cambio de flujo. En este captulo vamos a ver las estructuras que trasfieren el control a una nueva direccin, de acuerdo a una condicin evaluada.

208

Captulo 9. Estructuras de control I: Estructuras de seleccin.

Bifurcacin Abierta. La estructura condicional if.


Las estructuras de control condicionales que se van a ver son la bifurcacin abierta y la bifurcacin cerrada. Un esquema del flujo de ambas estructuras ha quedado recogido en la Figura 9.2. La sentencia que est precedida por la estructura de control

condicionada se ejecutar si la condicin de la estructura de control es verdadera; en caso contrario no se ejecuta la instruccin condicionada y continua el programa con la siguiente instruccin. En la Figura 9.2. se puede ver un esquema del comportamiento de la bifurcacin abierta. La sintaxis de la estructura de control condicionada abierta es la siguiente: if(condicin) sentencia; Si la condicin es verdadera (distinto de cero en el lenguaje C), se ejecuta la sentencia. Si la condicin es falsa (igual a cero en el lenguaje C), no se ejecuta la sentencia. Si en lugar de una sentencia, se desean condicionar varias de ellas, entonces se crea una sentencia compuesta mediante llaves. Ejemplo: Programa que solicite dos valores enteros y muestre el cociente: #include <stdio.h> int main(void) { short D, d; printf("Programa para dividir dos enteros...\n"); printf("Introduzca el dividendo ... "); scanf( %hd",&D); printf("Introduzca el divisor ... "); scanf( %hd",&d); if(d != 0) printf("%hu / %hu = %hu", D, d, D / d); return 0; }

209

Informtica Aplicada. Programacin en Lenguaje C.

Se efectuar la divisin nicamente en el caso en que se verifique la condicin de que d != 0.

Bifurcacin Cerrada. La estructura condicional if - else.


En una bifurcacin cerrada, la sentencia que est precedida por una estructura de control condicionada se ejecutar si la condicin de la estructura de control es verdadera; en caso contrario se ejecuta una instruccin alternativa. Despus de la ejecucin de una de las dos sentencias (nunca las dos), el programa contina la normal ejecucin de las restantes sentencias que vengan a continuacin. La sintaxis de la estructura de control condicionada cerrada es la siguiente: if(condicin) sentencia1; else sentencia2; Si la condicin es verdadera (distinto de cero en el lenguaje C), se ejecuta la sentencia llamada sentencia1. Si la condicin es falsa (igual a cero en el lenguaje C), se ejecuta la sentencia llamada sentencia2. Si en lugar de una sentencia, se desean condicionar varias de ellas, entonces se crea una sentencia compuesta mediante llaves. Ejemplo: El mismo programa anteriormente visto. Quedar mejor si se escribe de la siguiente forma: #include <stdio.h> int main(void) { short D, d; printf("Programa para dividir dos enteros...\n"); printf("Introduzca dividendo ... "); scanf( %hd",&D); printf("Introduzca el divisor .. "); scanf( %hd",&d); if(d != 0) printf("%hu / %hu = %hu", D, d, D / d);

210

Captulo 9. Estructuras de control I: Estructuras de seleccin.

else printf(No se puede realizar divisin por cero); return 0; } Se efectuar la divisin nicamente en el caso en que se verifique la condicin de que d != 0. Si el divisor introducido es igual a cero, entonces imprime en pantalla un mensaje de advertencia.

Anidamiento de estructuras condicionales.


Decimos que se produce anidamiento de estructuras de control cuando una estructura de control aparece dentro de otra estructura de control del mismo tipo. Tanto en la parte if como en la parte else, los anidamientos pueden llegar a cualquier nivel. De esa forma podemos elegir entre numerosas sentencias estableciendo las condiciones necesarias. Una estructura de anidamiento tiene, por ejemplo, la forma: if(expresin_1) /* { if(expresin_2) /* { if(expresin_3) /* sentencia_1; else /* sentencia_2; } else /* sentencia_3; } else /* sentencia_4; primer if */ segundo if */ tercer if */ alternativa al tercer if */ alternativa al 2 if */ alternativa al primer if */

(se puede ver el flujograma de este cdigo en la figura 9.4.) Cada else se asocia al if ms prximo en el bloque en el que se encuentre y que no tenga asociado un else. No est permitido (no tendra sentido) utilizar un else sin un if previo. Y la estructura else debe ir inmediatamente despus de la sentencia condicionada con su if.

211

Informtica Aplicada. Programacin en Lenguaje C.

C S S S No No No

C1

C2

C3

S1

S2

S3

S4

Figura 9.4.: Ejemplo de estructuras condicionales anidadas.

Un ejemplo de estructura anidada sera, siguiendo con los ejemplos anteriores, el caso de que, si el divisor introducido ha sido el cero, el programa preguntase si se desea introducir un divisor distinto. #include <stdio.h> int main(void) { short D, d; char opcion; printf("Programa para dividir dos enteros...\n"); printf("Introduzca dividendo ... "); scanf( %hd",&D); printf("Introduzca el divisor .. "); scanf( %hd",&d); if(d != 0) printf("%hu / %hu = %hu", D, d, D / d); else { printf("No se puede dividir por cero.\n"); printf("Introducir otro denominador (s/n)?"); opcion = getchar(); if(opcion == 's') { printf("\nNuevo denominador ... "); scanf( %hd",&d); if(d != 0) printf("%hu / %hu = %hu", D, d, D/d); else printf("De nuevo ha introducido 0."); } } return 0; }

212

Captulo 9. Estructuras de control I: Estructuras de seleccin.

La funcin getchar est definida en la biblioteca stdio.h. Esta funcin espera a que el usuario pulse una tecla del teclado y, una vez pulsada, devuelve el cdigo ASCII de la tecla pulsada. En el caso de que el usuario del programa introduzca el carcter s en la ejecucin de la funcin getchar, el programa le permitir introducir otro denominador. Si, de nuevo, el usuario introduce un cero, entonces el programa no har la divisin y terminar su ejecucin. En este ejemplo hemos llegado hasta un tercer nivel de anidacin.

Escala if else.
Cuando se debe elegir entre una lista de opciones, y nicamente una de ellas ha de ser vlida, se llega a producir una concatenacin de condiciones de la siguiente forma: if(condicin1) setencia1; else { if(condicin2) sentencia2; else { if(condicin3) sentencia3; else sentencia4; } } El flujograma recogido en la Figura 9.4. representara esta situacin sin ms que intercambiar los caminos de verificacin de las condiciones C1, C2 y C3 recogidas en l (es decir, intercambiando los rtulos de S y de No). Este tipo de anidamiento se resuelve en C con la estructura else if, que permite una concatenacin de las condicionales. Un cdigo como el antes escrito quedara: if(condicin1) sentencia1; else if (condicin2) sentencia2; else if(condicin3) sentencia3; else sentencia4;

213

Informtica Aplicada. Programacin en Lenguaje C.

Como se ve, una estructura as anidada se escribe con mayor facilidad y expresa tambin ms claramente las distintas alternativas. No es necesario abierta: if(condicin1) sentencia1; else if (condicin2) sentencia2; else if(condicin3) sentencia3; Un ejemplo de concatenacin podra ser el siguiente programa, que solicita al usuario la nota de un examen y muestra por pantalla la calificacin acadmica obtenida: #include <stdio.h> int main(void) { float nota; printf("Introduzca la nota del examen ... "); scanf( %f",&nota); if(nota < 0 || nota > 10) printf("Nota incorrecta."); else if(nota < 5) printf("Suspenso."); else if(nota < 7) printf("Aprobado."); else if(nota < 9) printf("Notable."); else if(nota < 10) printf("Sobresaliente."); else printf("Matrcula de honor."); return 0; } nicamente se evaluar un else if cuando no haya sido cierta la condicin de ninguno de los anteriores ni del if inicial. Si todas las condiciones resultan ser falsas, entonces se ejecutar (si existe) el ltimo else. que, en un anidamiento de sentencias condicionales, encontremos un else final: el ltimo if puede ser una bifurcacin

La estructura condicional y el operador condicional.


Existe un operador que selecciona entre dos opciones, y que realiza, de forma muy sencilla y bajo ciertas limitaciones la misma operacin de

214

Captulo 9. Estructuras de control I: Estructuras de seleccin.

seleccin que la estructura de bifurcacin cerrada. Es el operador interrogante, dos puntos (?:). La sintaxis del operador es la siguiente: expresin_1 ? expresin_2 : expresin3; Se evala expresin_1; si resulta ser verdadera (distinta de cero), entonces se ejecutar la sentencia recogida en expresin_2; y si es falsa (igual a cero), entonces Tanto se ejecutar como la sentencia recogida pueden en ser expresin_3. expresin_2 expresin_3

funciones, o expresiones muy complejas, pero siempre deben ser sentencias simples. Es conveniente no renunciar a conocer algn aspecto de la sintaxis de un lenguaje de programacin. Es cierto que el operador interrogante dos puntos se puede siempre sustituir por la estructura de control condicional if else. Pero el operador puede, en muchos casos, simplificar el cdigo o hacerlo ms elegante. Y hay que tener en cuenta que el resto de los programadores s hacen uso del operador y el cdigo en C est lleno de ejemplos de su uso. Por ejemplo, el cdigo: if(x >= 0) else printf(Positivo\n); printf(Negativo\n);

es equivalente a: printf(%s\n, x >= 0 ? Positivo: Negativo); El uso de este operador tambin permite anidaciones. Por ejemplo, al implementar el programa que, dada una nota numrica de entrada, muestra por pantalla la calificacin en texto (Aprobado, sobresaliente, etc.), podra quedar, con el operador interrogante dos puntos de la siguiente manera: #include <stdio.h> int main(void) {

215

Informtica Aplicada. Programacin en Lenguaje C.

short nota; printf("Nota obtenida ... "); scanf( %hd", &nota); printf("%s", nota < 5 ? "SUSPENSO" : nota < 7 ? "APROBADO" : nota < 9 ? "NOTABLE" : "SOBRESALIENTE"); return 0; }

Estructura de seleccin mltiple: switch.


La estructura switch permite transferir el control de ejecucin del programa a un punto de entrada etiquetado en un bloque de sentencias. La decisin sobre a qu instruccin del bloque se trasfiere la ejecucin se realiza mediante una expresin entera. La forma general de la estructura switch es: switch(expresin_del_switch) { case expresionConstante1: [sentencias;] [break;] case expresionConstante2: [sentencias;] [break;] [] case expresionConstanteN: [sentencias;] [break;] [default sentencias;] } Una estructura switch comienza con la palabra clave switch seguida de una expresin (expresin_del_switch) recogida entre parntesis. Si la expresin del switch no es entera entonces el cdigo dar error en tiempo de compilacin. El cuerpo de la estructura switch se conoce como bloque switch y permite tener sentencias prefijadas con las etiquetas case. Una etiqueta

216

Captulo 9. Estructuras de control I: Estructuras de seleccin.

case es una constante entera (variables de tipo char short long, con o sin signo). Si el valor de la expresin de switch coincide con el valor de una etiqueta case, el control se transfiere a la primera sentencia que sigue a la etiqueta. No puede haber dos case con el mismo valor de constante. Si no se encuentra ninguna etiqueta case que coincida, el control se transfiere a la primera sentencia que sigue a la etiqueta default. Si no existe esa etiqueta default, y no existe una etiqueta coincidente, entonces no se ejecuta ninguna sentencia del switch y se contina, si la hay, con la siguiente sentencia posterior a la estructura. Por ejemplo, para el cdigo que se muestra a continuacin, y cuyo flujograma queda recogido en la figura 9.5.: switch(a) { case 1: case 2: case 3: default: }

printf(UNO\t); printf(DOS\t); printf(TRES\t); printf(NINGUNO\n);

Si el valor de a es, por ejemplo, 2, entonces comienza a ejecutar el cdigo del bloque a partir de la lnea que da entrada el case 2:. Producir la siguiente salida por pantalla:

C UNO DOS TRES NINGUNO

Figura 9.5.: Flujograma del programa ejemplo con switch sin sentencias break.

217

Informtica Aplicada. Programacin en Lenguaje C.

DOS

TRES

NINGUNO.

Una vez que el control se ha trasferido a la sentencia que sigue a una etiqueta concreta, ya se ejecutan todas las dems sentencias del bloque switch, de acuerdo con la semntica de dichas sentencias. El que aparezca una nueva etiqueta case no obliga a que se dejen de ejecutar las sentencias del bloque. Si se desea detener la ejecucin de sentencias en el bloque switch, debemos transferir explcitamente el control al exterior del bloque. Y eso se realiza utilizando la sentencia break. Dentro de un bloque switch, la sentencia break transfiere el control a la primera sentencia posterior al switch. Ese es el motivo por el que en la sintaxis de la estructura switch se escriba (en forma opcional) las sentencias break en las instrucciones inmediatamente anteriores a cada una de las etiquetas. En el ejemplo anterior, si colocamos la sentencia break en cada case, el algoritmo cambia y toma la forma indicada en la Figura 9.6. Y el cdigo queda de la siguiente manera: switch(a) { case 1: printf(UNO); case 2: printf(DOS); case 3: printf(TRES); default: printf(NINGUNO); }

break; break; break;

C UNO DOS TRES NINGUNO

Figura 9.6.: Flujograma del programa ejemplo con switch con sentencias break.

218

Captulo 9. Estructuras de control I: Estructuras de seleccin.

Entonces la salida por pantalla, si la variable a tiene el valor 2 ser nicamente: DOS La ejecucin de las instrucciones que siguen ms all de la siguiente etiqueta case puede ser til en algunas circunstancias. Pero lo habitual ser que aparezca una sentencia break al final del cdigo de cada etiqueta case. Una sola sentencia puede tener ms de una etiqueta case. Queda claro en el siguiente ejemplo: #include <stdio.h> int main(void) { short int nota; printf("Introduzca la nota del examen ... "); scanf( %hd",&nota); switch(nota) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: case 10: default: } return 0; }

printf(SUSPENSO); break; printf(APROBADO); break; printf(NOTABLE); break; printf(SOBRESALIENTE); break; printf(MATRCULA DE HONOR); break; printf(Nota introducida errnea.);

219

Informtica Aplicada. Programacin en Lenguaje C.

En este ejemplo, los valores 0, 1, 2, 3, y 4, ofrecen el mismo comportamiento: imprimen la palabra SUSPENSO y abandonan el bloque switch. No se puede poner una etiqueta case fuera de un bloque switch. Y tampoco tiene sentido colocar instrucciones dentro del bloque switch antes de aparecer el primer case: eso supondra un cdigo que jams podra llegar a ejecutarse. Por eso, la primera sentencia de un bloque switch debe estar ya etiquetada. Se pueden anidar distintas estructuras switch. El ejemplo de las notas, que ya se mostr al ejemplificar una anidacin de sentencias ifelseif puede servir para comentar una caracterstica importante de la estructura switch. Esta estructura no admite, en sus distintas entradas case, ni expresiones lgicas o relacionales, ni expresiones aritmticas, sino literales. La nica relacin aceptada es, pues, la de igualdad. Y adems, el trmino de la igualdad es siempre entre una variable o una expresin entera (la del switch) y valores literales: no se puede indicar el nombre de una variable. El programa de las notas, si la variable nota hubiese sido de tipo float, como de hecho quedo definida cuando se resolvi el problema con los condicionales if elseif, no tiene solucin posible mediante la estructura switch. Y una ltima observacin: las sentencias de un case no forman un bloque y no tiene porqu ir entre llaves. La estructura switch completa, con todos sus cases, s es un bloque.

Recapitulacin.
Hemos presentado las estructuras de control existentes en el lenguaje C que permiten condicionar la ejecucin de una o varias sentencias, o elegir entre una o varias posibles: las estructuras condicionales de bifurcacin abierta o cerrada, condicionales anidadas, operador interrogante, dos puntos, y estructura switch.

220

Captulo 9. Estructuras de control I: Estructuras de seleccin.

Ejercicios.
En todos los ejercicios que planteamos a continuacin quiz ser conveniente que antes de abordar la implementacin se intente disear un algoritmo en pseudocdigo o mediante un diagrama de flujo. Si en algn caso el problema planteado supone especial dificultad quedar recogido ese flujograma en estas pginas.

9.1.

Presentar el algoritmo para un programa que resuelva una ecuacin de primer grado ( valores de los parmetros resultante de la variable . y ). El programa solicita los y debe mostrar el valor

1. [Entrada de datos] Leer y . 2. [Clculos] Si Entonces [Mostrar resultado]: Mostrar . Sino [es decir, ] 2.1. Entonces [Mostrar mensaje]: Mostrar No hay Ecuacin 3. Fin. El cdigo en C podra ser el siguiente: #include <stdio.h> int main(void) { short a, b; printf("Coeficiente a ... "); printf("Coeficiente b ... "); scanf( %hd", &a); scanf( %hd", &b);

if(a) printf("x = %lf\n", (double)(-b) / a); else printf("Ecuacin erronea\n"); return 0; } Desde luego, las variables otros tipos de datos. y podran haber sido declaradas como de

tipo double, u otro tipo. Puede usted probar a hacer el programa con

221

Informtica Aplicada. Programacin en Lenguaje C.

9.2.

Solicitar del usuario cuatro nmeros y mostrarlos por pantalla ordenados de menor a mayor.

#include <stdio.h> int main(void) { unsigned short int a0,a1,a2,a3; printf("Introduzca cuatro enteros ... \n\n"); printf("Primer entero ... "); scanf( %hu",&a0); printf("Segundo entero ... "); scanf( %hu",&a1); printf("Tercer entero ... "); scanf( %hu",&a2); printf("Cuarto entero ... "); scanf( %hu",&a3); if(a0 > a1) { a0 ^= a1; a1 ^= a0; a0 ^= a1; } if(a0 > a2) { a0 ^= a2; a2 ^= a0; a0 ^= a2; } if(a0 > a3) { a0 ^= a3; a3 ^= a0; a0 ^= a3; } if(a1 > a2) { a1 ^= a2; a2 ^= a1; a1 ^= a2; } if(a1 > a3) { a1 ^= a3; a3 ^= a1; a1 ^= a3;

222

Captulo 9. Estructuras de control I: Estructuras de seleccin.

} if(a2 > a3) { a2 ^= a3; a3 ^= a2; a2 ^= a3; } printf("\nOrdenados... \n"); printf("%hu <= %hu <= %hu <= %hu.", a0, a1, a2, a3); return 0; } Donde el cdigo que se ejecuta en cada estructura if intercambia los valores de las dos variables, como ya vimos en un ejemplo de un tema anterior.

9.3.

Escriba un programa que resuelva una ecuacin de segundo grado. Tendr como entrada los coeficientes , y de la ecuacin y ofrecer como resultado las dos soluciones reales o imaginarias, para el caso en que el discriminante sea negativo.

#include <stdio.h> #include <math.h> int main(void) { float a, b, c; double r; // introduccin de parmetros... printf("Introduzca los coeficientes...\n\n"); printf("a --> "); scanf( %f",&a); printf("b --> "); scanf( %f",&b); printf("c --> "); scanf( %f",&c); // Ecuacin de primer grado... if(a == 0) { // No hay ecuacin ... if(b == 0) printf("No hay ecuacin.\n"); else // S hay ecuacin de primer grado { printf("Ec. de primer grado.\n");

223

Informtica Aplicada. Programacin en Lenguaje C.

printf("Una nica solucin.\n"); printf("x1 --> %lf\n", -c / b); } } // Ecuacin de segundo grado. Soluciones imaginarias. else if ((r = b * b - 4 * a * c) < 0) { printf("Ecuacin sin soluciones reales.\n"); r = sqrt(-r); printf("x1: %lf + %lf * i\n",-b/(2*a), r/(2*a)); printf("x2: %lf + %lf * i\n",-b/(2*a),-r/(2*a)); } // Ecuacin de segundo grado. Soluciones reales. else { printf("Las soluciones son:\n"); r = sqrt(r); printf("\tx1 --> %lf\n", (-b + r) / (2 * a)); printf("\tx2 --> %lf\n", (-b - r) / (2 * a)); } return 0; } En este programa comprobamos, como se ha podido ver, las siguientes posibilidades: (1) Que el coeficiente a sea o no igual a cero. Si es cero, verificamos (2) que no lo sea tambin b: en ese caso, no habra ecuacin; si b no es cero, tenemos una sencilla ecuacin de primer grado y mostramos su nica solucin. Si a es distinto de cero, debemos calcular el discriminante; (3) si ste es negativo, entonces tendremos dos soluciones imaginarias; de lo contrario, tendremos dos soluciones reales.

9.4.

Indique que salida, por pantalla, ofrecer el siguiente cdigo.

#include <stdio.h> int main(void) { short a = 5, b = 7; long c = a < b ? b - a : a - b; switch(c)

224

Captulo 9. Estructuras de control I: Estructuras de seleccin.

{ case 1: printf("uno\t"); case 2: printf("dos\t"); case 3: printf("tres\t"); default: printf("error\t"); } return 0; } Esta sencilla pregunta presenta una expresin con el operador

interrogante dos puntos y, luego, un switch. Como se ve, el switch no tiene ninguna sentencia break, por lo que, una vez entremos en l, ejecutar todas las lneas de cdigo dentro de l. El valor de la variable c es igual a 2: el resultado de restar b a, que es la operacin que se le asigna a la variable c puesto que la expresin de control a < b es verdadera. Entramos pues en la segunda lnea case de la estructura switch. Y a partir de ese momento ejecutar todas las instrucciones, sin condicionar ninguna, pues de hecho ninguna est condicionada. La salida por pantalla ser, pues: dos tres error

9.5.

Escriba un programa que solicite al usuario las coordenadas dentro de la circunferencia centrada en est sobre ella, o si est fuera de la misma. Como ya sabr, los puntos y radio y de radio

de un punto del plano e indique por pantalla si ese punto est o si

de la circunferencia de centro .

verifican la ecuacin

Una posible solucin a esta pregunta sera la siguiente: #include <stdio.h> int main(void)

225

Informtica Aplicada. Programacin en Lenguaje C.

{ double x0, y0, r; double x, y; printf("Coord. centro: x0 = "); printf("Coord. centro: y0 = "); printf("radio = "); printf("Valor de x ... "); printf("Valor de y ... "); scanf( %lf", &x0); scanf( %lf", &y0); scanf( %lf", &r); scanf( %lf", &x); scanf( %lf", &y);

double p = (x - x0) * (x - x0) + (y - y0) *(y - y0); if(p == r * r) printf("El punto es de la circunferencia."); else if(p < r * r) printf("El punto es del circulo. "); else printf("El punto est fuera del circulo. "); return 0; }

9.6.

El calendario juliano (debido a julio Cesar) consideraba que el ao duraba 365.25 das, por lo que se estableci que los aos tendran una duracin de 365 das y cada cuatro aos se aadiese un da ms (ao bisiesto). Sin embargo se comprob que en realidad el ao tiene 365.2422 das, lo que implica que el calendario juliano llevase un desfase de unos once minutos. Este error es relativamente pequeo, pero, con el transcurrir del tiempo, el error acumulado puede ser importante. El papa Gregorio XII, en 1582, propuso reformar el calendario juliano para evitar los errores arrastrados de aos anteriores. Los acuerdos tomados entonces, que son por los que nos an nos regimos, fueron los siguientes: Para suprimir el error acumulado por el calendario juliano, se suprimieron diez das. De tal manera que el da siguiente al 4 de octubre de 1582 fuel el da 15 del mismo mes.

226

Captulo 9. Estructuras de control I: Estructuras de seleccin.

La duracin de los aos sera de 365 das o 366 en caso de ser bisiesto. Sern bisiestos todos los aos que sean mltiplos de 4, salvo los que finalizan en 00, que slo lo sern cuando tambin sean mltiplos de 400, por ejemplo 1800 no fue bisiesto y el 2000 s. Con esta reforma el desfase existente entre el ao civil y el ao real se reduce a menos de treinta segundos anuales. Definir un algoritmo que solicite al usuario una fecha

introducida mediante tres datos: da, mes y ao; ese programa debe validar la fecha: es decir comprobar que la fecha es correcta cumpliendo las siguientes reglas: El ao debe ser mayor que 0. El mes debe ser un nmero entre uno y doce. El da debe estar entre 1 y 30, 31,28 29 dependiendo el mes de que se trate y si el ao es bisiesto o no.

Una vez se hay introducido el valor del da (variable dd), del mes (variable mm) y del ao (variable aa), el algoritmo decidir si esa fecha es vlida o no, de acuerdo con las indicaciones recogidas en el enunciado. El algoritmo queda de la siguiente forma: 1. Si C1 Entonces: 2. Si no, Entonces 3. Si C2 Entonces: . (Ao bisiesto) . (Ao no bisiesto) Fecha Errnea. Fecha Errnea. Fecha Errnea. Fecha Correcta.

[Mostrar Mensaje]: Si no, Entonces: 3.1.1. Si C3 Entonces: [Mostrar Mensaje]: Si no, Entonces: 3.1.1.1. Si C4 Entonces: [Mostrar Mensaje]: Si no, Entonces [Mostrar Mensaje]:

227

Informtica Aplicada. Programacin en Lenguaje C.

4. Fin. Donde las condiciones resumidas son las recogidas a continuacin. C1 C2 ( (( C3 C4 y( )( )( )y( )y( y y( ( y ( )( )). )) y )( ) )).

Queda pendiente, como trabajo a resolver por el alumno, la confeccin del flujograma e implementar el cdigo.

9.7.

Escriba un programa que solicite al usuario que introduzca por teclado un da, mes y ao, y muestre entonces por pantalla el da de la semana que le corresponde.

El anterior enunciado presentaba un problema tambin de calendario. En aquel enunciado quedaba recogido el algoritmo para determinar si una fecha era correcta o no. Ahora el enunciado es muy breve. No recoge ninguna pista sobre cmo resolver el problema. Dejando de lado cualquier consideracin sobre la programacinCmo saber que el 15 de febrero de 1975 fue sbado? Porque si no se sabe cmo conocer los das de la semana de cualquier fecha, entonces nuestro problema no es de programacin, sino de algoritmo. Por lo tanto, antes de intentar implementar un programa que resuelva este problema, ser necesario preguntarse si somos capaces de resolverlo sin programa. Buscando en Internet es fcil encontrar informacin parecida a la siguiente: Para saber a qu da de la semana corresponde una determinada fecha, basta aplicar la siguiente expresin: .

228

Captulo 9. Estructuras de control I: Estructuras de seleccin.

Donde lunes,,

es el da de la semana ( es el sbado); es el ao de la fecha; y

es el domingo;

es el es el

es el da del mes de la fecha;

mes de la fecha;

es la centuria (es decir, los

dos primero dgitos del ao) de la fecha. A esos valores hay que introducirle unas pequeas modificaciones: se considera que el ao comienza en marzo, y que los meses de enero y febrero son los meses 11 y 12 del ao anterior. Hagamos un ejemplo a mano: Qu da de la semana fue el 15 de febrero de 1975?: . : hemos quedado que en nuestra ecuacin el mes de febrero es el dcimo segundo mes del ao anterior. : hemos quedado que el mes de febrero corresponde al ltimo mes del ao anterior. .

Con todos estos valores, el da de la semana queda:

. que es igual a 6, es decir, sbado. Slo queda hacer una ltima advertencia a tener en cuenta a la hora de calcular nuestros valores de que y de : Si queremos saber el da de la , que y semana del 1 de febrero de 2000, tendremos que y slo despus calcular los valores de y de

: es decir, primero convendr hacer las rectificaciones al ao . se da fue

. es decir martes!

229

Informtica Aplicada. Programacin en Lenguaje C.

Queda ahora hacer el programa que nos d la respuesta al da de la semana en el que estamos. Har falta emplear dos veces la estructura de control condicional if y una vez el switch. El programa queda como sigue: #include <stdio.h> int main(void) { unsigned short D, mm, aaaa; unsigned short M, A, C; printf("Introduzca la fecha ... printf("Da ... "); scanf( printf("Mes ... "); scanf( printf("Ao ... "); scanf( \n"); %hu", &D); %hu", &mm); %hu", &aaaa);

// Valores de las variables: // El valor de D ya ha quedado introducido por el usuario. // Valor de M: if(mm< 3) { M = A = C = } else { M = A = C = }

mm + 10; (aaaa - 1) % 100; (aaaa - 1) / 100;

mm - 2; aaaa % 100; aaaa / 100; %4hu fue ",D, mm, aaaa); + A/4 + C/4 - C*2 ) % 7) break; break; break; break; break; break;

printf("El da %2hu de %2hu de switch((70+(26*M-2)/10 + D + A { case 0: printf("DOMINGO"); case 1: printf("LUNES"); case 2: printf("MARTES"); case 3: printf("MIRCOLES"); case 4: printf("JUEVES"); case 5: printf("VIERNES"); case 6: printf("SBADO"); } return 0; }

230

Captulo 9. Estructuras de control I: Estructuras de seleccin.

Si, por ejemplo, introducimos la fecha 25 de enero de 1956, la salida del programa tendr el siguiente aspecto: Introduzca la fecha ... Da ... 25 Mes ... 1 Ao ... 1956 El da 25 de 1 de 1956 fue MIRCOLES Falta aclarar por qu he sumado 70 al valor de la expresin que calculamos en el switch. Veamos un ejemplo para justificar ese valor: supongamos la fecha 2 de abril de 2001. Tendremos que y , y entonces el valor de queda: , , que es igual a

. Nuestro algoritmo trabaja con valores entre 0 y 6, y al salir negativo el valor sobre el que se debe calcular el mdulo de 7, el resultado nos sale fuera de ese rango de valores. Pero la operacin mdulo establece una relacin de equivalencia entre el conjunto de los enteros y el conjunto de valores comprendidos entre 0 y el valor del mdulo menos 1. Le sumamos al valor calculado un mltiplo de 7 suficientemente grande para que sea cual sea el valor de las variables, al final obtenga un resultado positivo. As, ahora, el valor obtenido ser , es decir, lunes: Introduzca la fecha ... Da ... 2 Mes ... 4 Ao ... 2001 El da 2 de 4 de 2001 fue LUNES

231

Informtica Aplicada. Programacin en Lenguaje C.

232

CAPTULO 10
ESTRUCTURAS DE CONTROL II: ESTRUCTURAS DE REPETICIN O SENTENCIAS ITERADAS.
En el captulo anterior hemos vito las estructuras de control que permiten condicionar sentencias. En este captulo quedan recogidas las reglas sintcticas que se exige en el uso del lenguaje C para el diseo de estructuras de iteracin. Tambin veremos las sentencias de salto que nos permiten abandonar el bloque de sentencias iteradas por una estructura de control de repeticin.

Introduccin.
Una estructura de repeticin o de iteracin es aquella que nos permite repetir un conjunto de sentencias mientras que se cumpla una determinada condicin.

Informtica Aplicada. Programacin en Lenguaje C.

No S

I1

I1

I2

No
I2

Estructura while

Estructura do - while

Figura 10.1.: Estructuras de repeticin.

Las estructuras de iteracin o de control de repeticin, en C, se implementan con las estructuras dowhile, while y for. Todas ellas permiten la anidacin de unas dentro de otras a cualquier nivel. Puede verse un esquema de su comportamiento en la figura 9.3., que se present en las primeras pginas del captulo anterior y que repetimos ahora, en la figura 10.1.

Estructura while.
La estructura while se emplea en aquellos casos en que no se conoce por adelantado el nmero de veces que se ha de repetir la ejecucin de una determinada sentencia o bloque: ninguna, una o varias. La sintaxis de la estructura while es la que sigue: while(condicin) sentencia; donde condicin es cualquier expresin vlida en C. Esta expresin se evala cada vez, antes de la ejecucin de la sentencia iterada (o bloque de sentencias si se desea iterar una sentencia compuesta). Puede por tanto no ejecutarse nunca el bloque de sentencias de la estructura de control. Las sentencias se volvern a ejecutar una y otra vez mientras condicin siga siendo una expresin verdadera. Cuando la condicin resulta ser falsa, entonces el contador de programa se sita en la

234

Captulo 10. Estructuras de control II: Estructuras de iteracin.

inmediata siguiente instruccin posterior a la sentencia gobernada por la estructura. Veamos un ejemplo sencillo. Hagamos un programa que solicite un entero y muestre entonces por pantalla la tabla de multiplicar de ese nmero. El programa es muy sencillo gracias a las sentencias de repeticin: #include <stdio.h> int main(void) { short int n,i; printf("Tabla de multiplicar del ... "); scanf( %hd",&n); i = 0; while(i <= 10) { printf("%3hu * %3hu = %3hu\n",i,n,i * n); i++; } return 0; } Despus de solicitar el entero, inicializa a 0 la variable i y entonces, mientras que esa variable contador sea menor o igual que 10, va mostrando el producto del entero introducido por el usuario con la variable contador i. La variable i cambia de valor dentro del bucle de la estructura, de forma que llega un momento en que la condicin deja de cumplirse; cundo?: cuando la variable i tiene un valor mayor que 10. Conviene asegurar que en algn momento va a dejar de cumplirse la condicin; de lo contrario la ejecucin del programa podra quedarse atrapada en un bucle infinito. De alguna manera, dentro de la sentencia gobernada por la estructura de iteracin, hay que modificar alguno de los parmetros que intervienen en la condicin. Ms adelante, en este captulo, veremos otras formas de salir de la iteracin. Este ltimo ejemplo ha sido sencillo. Veamos otro ejemplo que requiere un poco ms de imaginacin. Supongamos que queremos hacer un

235

Informtica Aplicada. Programacin en Lenguaje C.

programa que solicite al usuario la entrada de un entero y que entonces muestre por pantalla el factorial de ese nmero. Ya se sabe la definicin de factorial: .

Antes de mostrar el cdigo de esta sencilla aplicacin, conviene volver a una idea comentada captulos atrs. Efectivamente, habr que saber decir en el lenguaje C cmo se realiza esta operacin. Pero previamente debemos ser capaces de expresar el procedimiento en castellano. En el captulo 5 se encuentra documentado ste y otros algoritmos de iteracin. Veamos una posible solucin al programa del factorial: #include <stdio.h> int main(void) { unsigned short n; unsigned long Fact; printf("Introduzca el entero ... "); scanf( %hu",&n); printf("El factorial de %hu es ... ", n); Fact = 1; while(n != 0) { Fact = Fact * n; n = n - 1; } printf("%lu.", Fact); return 0; } El valor de la variable Fact se inicializa a uno antes de comenzar a usarla. Efectivamente es muy importante no emplear esa variable sin darle el valor inicial que a nosotros nos interesa. La variable n se inicializa con la funcin scanf. Mientras que n no sea cero, se ir multiplicando Fact (inicialmente a uno) con n. En cada iteracin el valor de n se ir decrementando en uno.

236

Captulo 10. Estructuras de control II: Estructuras de iteracin.

La tabla de los valores que van tomando ambas variables se muestra en la tabla 10.1. (se supone, en esa tabla, que la entrada por teclado ha sido el nmero 5). Cuando la variable n alcanza el valor cero termina la iteracin. En ese momento se espera que la variable Fact tenga el valor que corresponde al factorial del valor introducido por el usuario.

n Fact

5 5

4 20

3 60

2 120

1 120

0 120

Tabla 10.1.: Valores que van tomando las variables del bucle del programa del clculo del factorial si la entrada del usuario ha sido n = 5.

La iteracin se ha producido tantas veces como el cardinal del nmero introducido. Por cierto, que si el usuario hubiera introducido el valor cero, entonces el bucle no se hubiera ejecutado ni una sola vez, y el valor de Fact hubiera sido uno, que es efectivamente el valor por definicin ( ).

La estructura de control mostrada admite formas de presentacin ms compacta y, de hecho, lo habitual ser que as se presente. Por ejemplo: while(n != 0) { Fact *= n; n = n - 1; } donde todo es igual excepto que ahora se ha hecho uso del operador compuesto en la primera sentencia del bloque. Pero an se puede compactar ms: 1. La condicin de permanencia ser verdad siempre que n no sea cero. Y por definicin de verdad en C (algo es verdadero cuando es

237

Informtica Aplicada. Programacin en Lenguaje C.

distinto de cero) se puede decir que la expresin n != 0 es verdadera s y slo si n es verdadero. 2. Las dos sentencias simples iteradas en el bloque pueden

condensarse en una sola: Fact *= n--; As las cosas, la estructura queda: while(n) Fact *= n--; Hacemos un comentario ms sobre la estructura while. Esta estructura permite iterar una sentencia sin cuerpo. Por ejemplo, supongamos que queremos hacer un programa que solicite continuamente del usuario que pulse una tecla, y que esa solicitud no cese hasta que ste introduzca el carcter, por ejemplo, a. La estructura quedar tan simple como lo que sigue: while((ch = getchar()) != a); Esta lnea de programa espera una entrada por teclado. Cuando sta se produzca comprobar que hemos tecleado el carcter a minscula; de no ser as, volver a esperar otro carcter. Una forma ms sencilla fcil de ver el significado de esta lnea de cdigo arriba escrita sera expresarlo de la siguiente manera: while(ch != a) ch = getchar(); Un ltimo ejemplo clsico de uso de la estructura while. El clculo del mximo comn divisor de dos enteros que introduce el usuario por teclado. No es necesario explicar el concepto de mximo comn divisor. S es necesario en cambio explicar un mtodo razonable de plantear al ordenador cmo se calcula ese valor: porque este ejemplo deja claro la importancia de tener no slo conocimientos de lenguaje de programacin, sino tambin de algoritmos vlidos.

238

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Euclides, matemtico del siglo V a. de C. present un algoritmo muy fcil de implementar, y de muy bajo coste computacional. Ya presentamos este algoritmo en el captulo 5; lo repetimos ahora. El algoritmo de Euclides dice que el mximo comn divisor de dos enteros y de (diremos y donde con y de buscado es ), donde , entendiendo por , , y es igual a donde el resto de la divisin . Entonces, el

. Y el proceso puede seguirse hasta llegar a unos valores de que verifiquen que .

algoritmo de Euclides afirma que, llegado a estos valores el valor

Ahora falta poner esto en lenguaje C. Ah va: #include <stdio.h> int main(void) { short int a, b, aux; short int mcd; printf("Valor de a ... "); scanf( %hd",&a); printf("Valor de b ... "); scanf( %hd",&b); printf("El mcd de %hd y %hd es ... ", a, b); while(b) { aux = a % b; a = b; b = aux; } printf("%hu", a); return 0; } Hemos tenido que emplear una variable auxiliar, que hemos llamado aux, para poder hacer el intercambio de variables: que a pase a valer el valor de b y b el del resto de dividir a por b. As como queda escrito el cdigo, se irn haciendo los intercambios de valores en las variables a y b hasta llegar a un valor de b igual a cero; entonces, el anterior valor de b (que est guardado en a) ser el mximo comn divisor.

239

Informtica Aplicada. Programacin en Lenguaje C.

Estructura do while.
La estructura dowhile es muy similar a la anterior. La diferencia ms sustancial est en que con esta estructura el cdigo de la iteracin se ejecuta, al menos, una vez. Si despus de haberse ejecutado, la condicin se cumple, entonces vuelve a ejecutarse, y as hasta que la condicin no se cumpla. Puede verse un esquema de su comportamiento en la figura 10.1., en pginas anteriores. La sintaxis de la estructura es la siguiente: do sentencia; while(condicin); Y si se desea iterar un bloque de sentencias, entonces se agrupan en una sentencia compuesta mediante llaves. Habitualmente, toda solucin a un problema resuelto con una

estructura, es tambin solventable, de forma ms o menos similar, por cualquiera de las otras dos estructuras. Por ejemplo, la tabla de multiplicar quedara: #include <stdio.h> int main(void) { short int n,i = 0; printf("Tabla de multiplicar del ... "); scanf( %hd",&n); do { printf("%3hu * %3hu = %3hu\n",i,n,i * n); i++: }while(i <= 10); return 0; } Una estructura muy habitual en un programa es ejecutar unas instrucciones y luego preguntar al usuario, antes de terminar la ejecucin de la aplicacin, si desea repetir el proceso. Supongamos, por

240

Captulo 10. Estructuras de control II: Estructuras de iteracin.

ejemplo, que queremos un programa que calcule el factorial de tantos nmeros como desee el usuario, hasta que no quiera continuar. El cdigo ahora requiere de otra estructura de repeticin, que vuelva a ejecutar el cdigo mientras que el usuario no diga basta. Una posible codificacin de este proceso sera (ver figura 10.2.): #include <stdio.h> int main(void) { unsigned short n; unsigned long Fact; char opcion; do { Fact = 1; printf("\n\nIntroduzca el entero ... "); scanf( %hu",&n); printf("El factorial de %hu es ... ", n); while(n != 0) Fact *= n--; printf("%lu.", Fact); printf("\n\nCalcular otro factorial (s/n) "); }while(opcion = getchar() == 's'); }

PROCESO S No

repetir

Figura 10.2.: Flujograma F para repeticin de proceso.

La estructura dowhile repetir el cdigo que calcula el factorial del entero solicitado mientras que el usuario responda con una s a la pregunta de si desea que se calcule otro factorial. Podemos afinar un poco ms en la presentacin. Vamos a rechazar cualquier contestacin que no sea o s o n: si el usuario responde s, entonces se repetir la ejecucin del clculo del factorial; si responde n

241

Informtica Aplicada. Programacin en Lenguaje C.

el programa terminar su ejecucin; y si el usuario responde cualquier otra letra, entonces simplemente ignorar la respuesta y seguir esperando una contestacin vlida. El cdigo queda ahora de la siguiente manera: #include <stdio.h> int main(void) { unsigned short n; unsigned long Fact; char opcion; do { Fact = 1; printf("\n\nIntroduzca el entero ... "); scanf( %hu",&n); printf("El factorial de %hu es ... ", n); while(n != 0) Fact *= n--; printf("%lu.", Fact); printf("\n\nCalcular otro factorial (s/n) "); do opcion = getchar(); while (opcion != s && opcion != n); }while(opcion == 's'); return 0;

Figura 10.3.: Flujograma para repeticin de proceso.

PROCESO

opcion S No S C2 No

C1

. .

242

Captulo 10. Estructuras de control II: Estructuras de iteracin.

} Ahora el valor de la variable opcion se ir pidiendo mientras que el usuario no introduzca correctamente una de las dos respuestas vlidas: o s (s), o no (n). El flujograma de esta nueva solucin queda recogido en la figura 10.3.

Estructura for.
Una estructura for tiene una sintaxis notablemente distinta a la indicada para las estructuras while y dowhile. Pero la actuacin que realiza sobre el cdigo es la misma. Con la palabra reservada for podemos crear estructuras de control que se dicen controladas por variable. La sintaxis de la estructura for es la siguiente: for(sentencias_1 , expresin ; sentencias_2) sentencia_3; Donde sentencia3 es la sentencia que se itera, la que queda gobernada por la estructura de control for. Donde sentencias_1 es un grupo de sentencias que se ejecutan antes que ninguna otra en una estructura for, y siempre se ejecutan una vez y slo una vez. Son sentencias, separadas por el operador coma, de inicializacin de variables.

s1

Figura 10.4.: Flujograma la estructura de control for(s1 ; e1 ; s2) s3;

S s3

e1

No

F s2

243

Informtica Aplicada. Programacin en Lenguaje C.

Donde expresin es la condicin de permanencia en la estructura for. Siempre que se cumpla expresin volver a ejecutarse la sentencia iterada por la estructura for. Donde sentencias_2 son un grupo de sentencias que se ejecutan despus de la sentencia iterada (despus de sentencia_3). El orden de ejecucin es, por tanto (ver flujograma en figura 10.4.): Se inicializan variables segn el cdigo recogido en sentencias_1 (s1). 1. Se verifica la condicin de permanencia recogida en expresin (e1). Si expresin es verdadero se sigue en el paso 3; si es falso entonces se sigue en el paso 6. 2. Se ejecuta la sentencia iterada llamada, en nuestro esquema de sintaxis, sentencia_3 (s3). 3. Se ejecutan las sentencias recogidas en sentencias_2 (s2.) 4. Vuelta al paso 2. 5. Fin de la iteracin. As, una estructura for es equivalente a una estructura while de la forma: sentencias_1; while(expresin) { sentencia_3; sentencias_2; } Por ejemplo, veamos un programa que muestra por pantalla los enteros pares del 1 al 100: #include <stdio.h> int main(void) { short i; for(i = 2 ; i <= 100 ; i += 2) printf("%5hd",i);

244

Captulo 10. Estructuras de control II: Estructuras de iteracin.

return 0; } Si queremos mejorar la presentacin, podemos hacer que cada cinco pares comience una nueva fila: #include <stdio.h> int main(void) { short i; for(i = 2 ; i <= 100 ; i += 2) { printf("%5hd",i); if(i % 10 == 0) printf("\n"); } return 0; } Ya hemos dicho que en cada uno de los tres espacios de la estructura for destinados a recoger sentencias o expresiones, pueden consignarse una expresin, o varias, separadas por comas, o ninguna. Y la sentencia iterada mediante la estructura for puede tener cuerpo, o no. Veamos por ejemplo, el clculo del factorial de un entero mediante una estructura for: for(Fact = 1 ; n ; Fact *= n--); Esta estructura for no itera ms que la sentencia punto y coma. Toda la trasformacin que deseamos realizar queda en la expresin del clculo del factorial mediante la expresin Fact *= n--. En este caso especial, el punto y coma debe ponerse: toda estructura de control acta sobre una sentencia. Si no queremos, con la estructura for, controlar nada, entonces la solucin no es no poner nada, sino poner una sentencia vaca. Todos los ejemplos que hasta el momento hemos puesto en la presentacin de las estructuras while y do while se pueden rehacer con una estructura for. En algunos casos es ms cmodo trabajar con la

245

Informtica Aplicada. Programacin en Lenguaje C.

estructura for; en otros se hace algo forzado. Veamos algunos ejemplos implementados ahora con la estructura for: Cdigo para ver la tabla de multiplicar: #include <stdio.h> int main(void) { short int n,i; printf("Tabla de multiplicar del ... "); scanf( %hd",&n); for(i = 0 ; i <= 10 ; i++) printf("%3hu * %3hu = %3hu\n",i,n,i * n); return 0; } Bloquear la ejecucin hasta que se pulse la tecla a: for( ; ch != a ; ch = getchar()); Bsqueda del mximo comn divisor de dos enteros: for( ; b ; ) { aux = a % b; a = b; b = aux; } printf("%hu", a);

Sentencias de salto: break y continue.


Hay dos sentencias que modifican el orden del flujo de instrucciones dentro de una estructura de iteracin. Son las sentencias break y continue. Ambas sentencias, en una estructura de iteracin, se presentan siempre condicionadas. Una sentencia break dentro de un bloque de instrucciones de una estructura de iteracin interrumpe la ejecucin de las restantes sentencias iteradas y abandona la estructura de control, asignando al contador de programa la direccin de la siguiente sentencia posterior a la llave que cierra el bloque de sentencias de la estructura de control.

246

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Por ejemplo, podemos hacer un programa que solicite al usuario que vaya introduciendo nmeros por teclado hasta que la entrada sea un nmero par. En ese momento el programa abandonar la solicitud de datos e indicar cuntos enteros ha introducido el usuario hasta introducir uno que sea par. El cdigo podra quedar as: #include <stdio.h> int main(void) { short i, num; for(i = 1 ; ; i++) // Sin condicin de permanencia. { printf("Introduzca un entero ... "); scanf( %hd",&num); if(num % 2 == 0) break; } printf("Ha introducido el entero par %hd",num); printf(" despus de %hd impares. ",i - 1); return 0; } Se habrn introducido tantos enteros como indique la variable i. De ellos, todos menos el ltimo habrn sido impares. Desde luego, el cdigo hara lo mismo si la expresin que condiciona al break se hubiese colocado en el segundo espacio que ofrece la sintaxis del for (all donde se colocan las condiciones de permanencia) y se hubiese cambiado en algo el cdigo; pero no resulta intuitivamente tan sencillo. Si comparamos la estructura for vista arriba con otra similar, en la que la condicin de salto queda recogida en el for tendremos: for(i = 1 ; ; i++) { printf("entero: "); scanf( %hd",&num); if(num % 2 == 0) break; } for(i = 1 ; num%2 == 0 ; i++) { printf("entero: "); scanf( %hd",&num); }

Aparentemente ambos cdigos hacen lo mismo. Pero si comparamos sus flujogramas recogidos en la figura 10.5. veremos que se ha introducido una diferencia no pequea. Habitualmente esta sentencia break siempre podr evitarse con un diseo distinto de la estructura de control. Segn en qu ocasiones, el

247

Informtica Aplicada. Programacin en Lenguaje C.

C1 Con break F

C1 Sin break F

Figura 10.5.: Salto con o sin break. cdigo adquiere mayor claridad si se utiliza el break. El uso de la sentencia de salto break es prctica habitual en la programacin estructurada como es el caso del paradigma del lenguaje C. Con la sentencia break es posible definir estructuras de control sin condicin de permanencia, o con una condicin que es siempre verdadera. Por ejemplo: long int suma = 0, num; do { printf(Introduzca un nuevo sumando ... ); scanf( %ld,&num); if(num % 2 != 0) break; suma += num; }while(1); printf(La suma es ... %ld, suma); En esta estructura, la condicin de permanencia es verdadera siempre, por definicin, puesto que es un literal diferente de cero. Pero hay una sentencia break condicionada dentro del bloque iterado. El cdigo ir guardando en la variable suma la suma acumulada de todos los valores introducidos por teclado mientras que esos valores sean enteros pares. En cuanto se introduzca un entero impar se abandona la estructura de iteracin y se muestra el valor sumado. El diagrama de flujo de este ltimo ejemplo queda recogido en la figura 10.6.

248

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Figura 10.6.: Otro ejemplo de salto con break.

C F

C1

Tambin se pueden tener estructuras for sin ninguna expresin recogida entre sus parntesis. Por ejemplo:

for( ; ; ) { ch = getchar(); printf(Esto es un bucle infinito\n); if(ch == a) break; } que repetir la ejecucin del cdigo hasta que se pulse la tecla a, y que ocasionar la ejecucin del break. Una sentencia continue dentro de un bloque de instrucciones de una estructura de iteracin interrumpe la ejecucin de las restantes sentencias iteradas y vuelve al inicio de las sentencias de la estructura de control, si es que la condicin de permanencia as lo permite. Veamos, por ejemplo, el programa antes presentado que muestra por pantalla los 100 primeros enteros pares positivos. Otro modo de resolver ese programa podra ser el siguiente: #include <stdio.h> int main(void) { short i; for(i = 1 ;i <= 100 ; i++) { if(i % 2) continue; printf("%4hd\t",i); }

249

Informtica Aplicada. Programacin en Lenguaje C.

return 0; } Cuando el resto de la divisin entera entre el contador i y el nmero 2 es distinto de cero, entonces el nmero es impar y se solicita que se ejecute la sentencia continue. Entonces se abandona la ejecucin del resto de las sentencias del bloque iterado y, si la condicin de permanencia en la iteracin as lo permite, vuelve a comenzar la iteracin por su primera sentencia del bloque iterado. El diagrama de

. .
C1 C2 F C

Figura 10.7.: Salto con continue. flujo del cdigo escrito queda recogido en la figura 10.7.

Palabra reservada goto.


La palabra reservada de C goto no debe ser empleada. La sentencia de salto goto no respeta las reglas de la programacin estructurada. Todo cdigo que se resuelve empleando una sentencia goto puede encontrar una solucin mejor con una estructura de iteracin. Si alguna vez ha programado con esta palabra, la recomendacin es que se olvide de ella. Si nunca lo ha hecho, la recomendacin es que la ignore y no se la aprenda.

250

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Y, eso s: hay que acordarse de que esa palabra es clave en C: no se puede generar un identificador con esa cadena de letras.

Variables de control de iteraciones.


Hasta el momento, hemos hecho siempre uso de las variables para poder almacenar valores concretos. Pero pueden tener otros usos. Por ejemplo, podemos hacer uso de ellas como chivatos o centinelas: su informacin no es el valor concreto numrico que puedan tener, sino la de una situacin del proceso. Como vamos viendo en los distintos ejemplos que se presentan, el diseo de bucles es tema delicado: acertar en la forma de resolver un problema nos permitir llevar solucin a muy diversos problemas. Pero, como estamos viendo, es importante acertar en un correcto control del bucle: decidir bien cuando se ejecuta y cuando se abandona. Existen dos formas habituales de controlar un bucle o iteracin: 1. Control mediante variable contador. En ellos una variable se encarga de contar el nmero de veces que se ejecuta el cuerpo del bucle. Esos contadores requieren una inicializacin previa, externa al bucle, y una actualizacin en cada iteracin para llegar as finalmente a un valor que haga falsa la condicin de permanencia. Esa actualizacin suele hacerse al principio o al final de las sentencias iteradas. Hay que garantizar, cuando se disea una iteracin, que se llega a una situacin de salida. 2. Control por suceso. Este tipo de control acepta, a su vez, diferentes modalidades: 2.1. Consulta explcita: El programa interroga al usuario si desea continuar la ejecucin del bucle. La contestacin del usuario normalmente se almacena en una variable tipo char o int. Es el

251

Informtica Aplicada. Programacin en Lenguaje C.

usuario, con su contestacin, quien decida la salida de la iteracin. 2.2. Centinelas: la iteracin se termina cuando la variable de control toma un valor determinado. Este tipo de control es usado habitualmente para introducir datos. Cuando el usuario introduce el valor que el programador ha considerado como valor de fin de iteracin, entonces, efectivamente, se termina esa entrada de datos. As lo hemos visto en el ejemplo de introduccin de nmeros, en el programa del clculo de la media, en el que hemos considerado que la introduccin del valor cero era entendido como final de la introduccin de datos. 2.3. Banderas: Es similar al centinela, pero utilizando una variable lgica que toma un valor u otro en funcin de determinadas condiciones que se evalan durante la ejecucin del bucle. Normalmente la bandera siempre se puede sustituir por un centinela, pero se emplea en ocasiones donde la condicin de terminacin resulta compleja y depende de varios factores que se determinan en diferentes puntos del bucle. En los ejercicios planteados al final del captulo podr encontrar algunos ejemplos de variables bandera y centinela.

Recapitulacin.
Hemos presentado las estructuras de control iterativas creadas con las estructuras for, while y do while, y las modificaciones a las estructuras que podemos introducir gracias a las palabras reservadas break y continue.

Ejercicios.

252

Captulo 10. Estructuras de control II: Estructuras de iteracin.

10.1.

Calcular la suma de los pares positivos menores o igual a 200.

Con una estructura for, incrementando de dos en dos el valor de la variable de control a la que se le asigna un valor inicial igual a 2, el cdigo resulta sencillo. Simplemente debemos definir una variable que guarde la suma acumulada de todos los valores que va tomando la variable de control. El cdigo puede ser algo as como lo que a continuacin se muestra: #include <stdio.h> int main(void) { long suma = 0; short i; for(i = 2 ; i <= 200 ; i += 2) suma += i; printf("esta suma es ... %ld.\n",suma); return 0; }

10.2.

Indique la salida que, por pantalla, ofrece el siguiente trozo de cdigo.

short a; while(a) scanf( %hd", &a); printf("El valor de a es ... %hd", a);

La salida que, por pantalla, ofrece este cdigo es: El valor de a es ... 0

10.3.

Muestre la salida que, por pantalla, ofrecer el siguiente programa.

253

Informtica Aplicada. Programacin en Lenguaje C.

#include <stdio.h> int main(void) { short a, b; for( a = 35 , b = 2 ; ; b++) if(a % b) continue; else break; printf("El valor de b es ... %hd", b); return 0; } Este cdigo simplemente usa, dentro de una estructura for, las sentencias de salto break y continue. Mientras que el resto de dividir a con b sea distinto de cero se ir ejecutando la sentencia continue. Se ejecutar la instruccin del tercer espacio de instrucciones del for (la instruccin b++) y se volver a entrar en el for: no hay expresin de condicin de permanencia, y siempre se iterar de nuevo salvo que se termine el proceso con una sentencia break; y sta llega en la primera ocasin en que se alcanza un valor de b que divida al valor de la variable a: cuando b vale 5. Por eso, el valor 5, ser lo que muestre este programa por pantalla.

10.4.

Muestre la salida que, por pantalla, ofrecer el siguiente programa.

include <stdio.h> int main(void) { short a = 210, b = 2; while(b <= a) { if(a % b) b++; else { printf("%hd\t", b); a /= b; }

254

Captulo 10. Estructuras de control II: Estructuras de iteracin.

} return 0; } Este programa lo que hace es obtener los factores primos del valor entero asignado a la variable a. En este caso, pues, la salida por pantalla ser la secuencia de enteros: 2 3 5 7.

Para ver el proceso que sigue un programa es muy til seguir las trazas de las variables. En este caso, las variables toman los valores iniciales , y . Y mientras que b sea menor o igual que a se realiza la siguiente operacin: si el valor de b NO divide al valor de a (es decir, si el resto es distinto de cero, lo que en C se interpreta como verdadero), entonces se incrementa el valor de b. Si S lo divide, entonces se muestra ese valor de b por pantalla y se cambia el valor de a, que pasa a ser ahora el cociente entre a y b: no se incrementa, en ese caso, en valor de b. La traza de las variables ser, por tanto, la siguiente:

a b

210 2 *

105 2

105 3 *

35 3

35 5 *

7 5

7 7 *

1 7

Donde hemos marcado con un asterisco los valores en los que se ejecuta la funcin printf. El proceso termina cuando se realiza la ltima divisin y el valor de a (1) pasa a ser menor que el valor de b (7).

10.5.

Muestre la salida que, por pantalla, ofrece el siguiente cdigo.

#include <stdio.h> int main(void) {

255

Informtica Aplicada. Programacin en Lenguaje C.

short a = 29; while(a) { a /= 2; printf("%5hd", a); } return 0; }

La salida por pantalla ser la siguiente:


14 7 3 1 0

10.6.

Hacer un programa que calcule la media de todos los valores que introduzca el usuario por consola. El programa debe dejar de solicitar valores cuando el usuario introduzca el valor 0.

#include <stdio.h> int main(void) { long suma; short i, num; for(i = 0, suma = 0 ; ; i++) { printf("Introduzca nmero ... "); scanf( %hd",&num); suma += num; if(num == 0) break; } if(i == 0) printf("No se han introducido enteros."); else printf("La media es ... %.2f.",(float)suma / i); return 0; } En la estructura for se inicializan las variables i y suma. Cada vez que se introduce un nuevo entero se suma al acumulado de todas las sumas de los enteros anteriores, en la variable suma. La variable i lleva la cuenta de cuntos enteros se han introducido; dato necesario para calcular, cuando se termine de introducir enteros, el valor de la media.

256

Captulo 10. Estructuras de control II: Estructuras de iteracin.

10.7.

Mostrar por pantalla todos los caracteres ASCII.

(Antes de ejecutar el cdigo escrito, termine de leer todo el texto recogido en este problema.) #include <stdio.h> int main(void) { unsigned char a; for(a = 0 ; a <= 255 ; a++) printf("%3c - %hX - %hd\n",a,a,a); return 0; } Va mostrando todos los caracteres, uno por uno, y su cdigo en hexadecimal y en decimal. Si se desea ver la aparicin de todos los caracteres, se puede programar para que se pulse una tecla cada vez que queramos que salga el siguiente carcter por pantalla. Simplemente habra que modificar la estructura for, de la siguiente forma: for(a = 0 ; a <= 255 ; a++) { printf("%3c - %hX - %hd\n",a,a,a); getchar(); } Advertencia importante: De forma intencionada hemos dejado el cdigo de este problema con un error grave. Aparentemente todo est bien. De hecho no existe error sintctico alguno. El error se ver en tiempo de ejecucin, cuando se compruebe que se ha cado en un bucle infinito. Para comprobarlo basta observar la condicin de permanencia en la iteracin gobernada por la estructura for, mediante la variable que hemos llamado a: a <= 255. Si se tiene en cuenta que la variable a es de tipo unsigned char, no es posible que la condicin indicada llegue a

257

Informtica Aplicada. Programacin en Lenguaje C.

ser falsa, puesto que, en el caso de que a alcance el valor 255, al incrementar en 1 su valor, incurrimos en overflow, y la variable cae en el valor cero: (255)10 (11111111)2 ; si solo tenemos ocho dgitos, entonces (11111111 1)2 (00000000)2 , pues no existe el bit noveno, donde debera haber quedado codificado un dgito 1. Sirva este ejemplo para advertir de que a veces puede aparecer un bucle infinito de la manera ms insospechada. La tarea de programar no est exenta de sustos e imprevistos que a veces obligan a dedicar un tiempo no pequeo a buscar la causa de un error. Un modo correcto de codificar este bucle sera, por ejemplo: for(a = 0 ; a < 255 ; a++) printf("%3c - %hX - %hd\n",a,a,a); printf("%3c - %hX - %hd\n",a,a,a); Y as, cuando llegue al valor mximo codificable en la variable a, abandona el bucle e imprime, ya fuera de la iteracin, una ltima lnea de cdigo, con el valor ltimo.

10.8.

Solicitar al usuario un valor entero por teclado y mostrar entonces, por pantalla, el cdigo binario en la forma en que se guarda el nmero en la memoria.

#include <stdio.h> int main(void) { signed long a; unsigned long Test; char opcion; do { Test = 0x80000000; printf("\n\nIndique el entero ... "); scanf( %ld", &a); while(Test) { Test & a ? printf("1") : printf("0");

258

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Test >>= 1; } printf("\nDesea introducir otro entero? ... "); do opcion = getchar(); while (opcion != 's' && opcion != 'n'); }while(opcion == 's'); return 0; } La variable Test se inicializa al valor hexadecimal 80000000, es decir, con un 1 en el bit ms significativo y en cero en el resto. Posteriormente sobre esta variable se va operando un desplazamiento a derecha de un bit cada vez. As, siempre tenemos localizado un nico bit en la codificacin de la variable Test, hasta que este bit se pierda por la parte derecha en un ltimo desplazamiento. Antes de cada desplazamiento, se realiza la operacin and a nivel de bit entre la variable Test, de la que conocemos donde est su nico bit, y la variable de la que queremos conocer su cdigo binario. En el principio, tendremos as las dos variables: Test a Test & a 1000 0000 0000 0000 0000 0000 0000 0000 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx x000 0000 0000 0000 0000 0000 0000 0000

Tenemos certeza de que la operacin and dejar un cero en todos los bits (menos el ms significativo) de Test & a, porque Test tiene todos esos bits a cero. Todos menos el primero. Entonces la operacin dar un valor distinto de cero nicamente si en ese bit se encuentra un 1 en la variable a. Sino, el resultado de la operacin ser cero. Al desplazar ahora Test un bit a la derecha tendremos la misma situacin que antes, pero ahora el bit de a testeado ser el segundo por la derecha. Test a Test & a 0100 0000 0000 0000 0000 0000 0000 0000 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx 0x00 0000 0000 0000 0000 0000 0000 0000

259

Informtica Aplicada. Programacin en Lenguaje C.

Y as sucesivamente, imprimiremos un 1 cuando la operacin and sea diferente de cero, e imprimiremos un 0 cuando la operacin and d un valor igual a cero. El programa que hemos presentado permite al usuario ver el cdigo de tantos nmeros como quiera introducir por teclado: hay una estructura dowhile que anida todo el proceso.

10.9.

Escribir un programa que solicite al usuario un entero positivo e indique si ese nmero introducido es primo o compuesto. El procedimiento ms usado para determinar esa propiedad de los nmeros, cuando estos son menores de diez millones, es la bsqueda exhaustiva de un entero que resulte ser divisor propio del nmero analizado. Si entre el 2 y la raz cuadrada del entero analizado no hay ningn divisor, entonces ese nmero es primo.

[Entrada de datos]: Leer

1. [Inicializar variables] , . 2. Mientras y Repetir: 2.1. Si Entonces: 2.1.1. 2.2. 3. Si Entonces 3.1. [Mostrar Resultado] Mostrar es Primo. Sino Entonces: 3.2. [Mostrar Resultado] Mostrar es Compuesto. 4. Fin. El Flujograma de este algoritmo puede verse en el Cuadro que hemos llamado Flujograma 10.8. La variable primo hace el papel de una variable chivato: su valor slo sirve para determinar si ha pasado algo dentro de una estructura de control iterada una vez ya hemos salido de ella.

260

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Leer

S S No

C1 S Mostrar Primo

No No

C2

C3

Mostrar Compuesto F

Flujograma 10.8.: Correspondiente al enunciado n. 5

C1 C2 C3

y ? ?

El realidad esa variable no hubiera sido necesaria. El algoritmo funcionara de la misma manera si eliminamos la condicin C2 y la sentencia que condiciona. La condicin de permanencia en la iteracin (la condicin C1) sera sera: . y . Y la condicin C3

Queda como trabajo personal rehacer el nuevo flujograma y el nuevo pseudocdigo. De acuerdo con el algoritmo propuesto en el flujograma 9.13., el cdigo del programa podra ser el siguiente: #include <stdio.h> #include <math.h> int main(void) { unsigned long int numero, raiz; unsigned long int div; char primo; printf("Dame el numero que vamos a testear ... "); scanf( %lu", &numero); primo = 0; raiz = sqrt(numero);

261

Informtica Aplicada. Programacin en Lenguaje C.

for(div = 2 ; div <= raiz ; div++) { if(numero % div == 0) { primo = 1; break; } } if(primo == 1) printf("El numero %lu es compuesto",numero); else printf("El numero %lu es primo",numero); return 0; } Antes de explicar brevemente el algoritmo, conviene hacer una digresin sencilla matemtica: todo entero verifica que, si tiene divisores distintos del 1 y del mismo nmero (es decir, si es compuesto), al menos uno de esos divisores es menor que su raz cuadrada. Eso es sencillo de demostrar por reduccin al absurdo: supongamos que tenemos un entero ejemplo, y y que tiene dos factores distintos de 1 y de ; por . Supongamos que ambos factores . , es decir,

son mayores (estrictamente mayores) que la raz cuadrada de Entonces tendremos: . En tal caso tendramos el absurdo de que . Por lo tanto, para saber si un entero buscar divisores entre 2 y

es estrictamente menor que es primo o compuesto, basta

. Si en ese rango no los encontramos,

entonces podemos concluir que el entero es primo. En este programa vamos probando con todo los posibles enteros que dividen al nmero estudiado, que sern todos los comprendidos entre el 2 y la raz cuadrada de ese nmero. En cuanto se encuentra un valor que divide al entero introducido, entonces ya est claro que ese nmero es compuesto y no es menester seguir buscando otros posibles divisores. Por eso se ejecuta la sentencia break.

262

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Al terminar la ejecucin de la estructura for no sabremos si hemos salido de ella gracias a que hemos encontrado un divisor y nos ha expulsado la sentencia break, o porque hemos terminado de testear entre todos los posibles candidatos a divisores menores que la raz cuadrada y no hemos encontrado ninguno porque el entero introducido es primo. Por ello hemos usado la variable chivato, que se pone a 1, antes de la sentencia break, en caso de que hayamos encontrado un divisor. Otro modo de saber si hemos salido del bucle por haber encontrado un divisor o por haber terminado el recorrido de la variable de control de la estructura for es verificar el valor de esa variable contador. Si la variable div es mayor que raiz entonces est claro que hemos salido del bucle por haber terminado la bsqueda de posibles divisores y numero es primo. Si div es menor o igual que raiz, entonces est tambin claro que hemos encontrado un divisor: es otra forma de hacer el programa, sin necesidad de crear la variable chivato. El cdigo, en ese caso, podra quedar de la siguiente manera: #include <stdio.h> #include <math.h> int main(void) { unsigned long int n, raiz; unsigned long int div; printf("Dame el numero que vamos a testear ... "); scanf( %lu", &n); raiz = sqrt(n); for(div = 2 ; div<=raiz ; div++) if(n%div == 0) break; printf(%lu es %s, n,n > raiz ? primo:compuesto); return 0; }

263

Informtica Aplicada. Programacin en Lenguaje C.

10.10.

Presentar un algoritmo que reciba un entero y muestre todos los enteros que lo dividen. Tenga en cuenta que todos los divisores de un entero excepto l mismo, se encuentran entre el 1 y la mitad del entero del que se buscan los divisores.

Con las indicaciones recogidas en el enunciado, el algoritmo tiene fcil presentacin: 1. [Entrada de datos]: Leer (El nmero a factorizar) 2. [Mostrar Divisor]: Mostrar 1. 3. Para Hasta , Repetir: 3.1. Si Entonces [Mostrar Divisor]: Mostrar . 3.2. . 4. [Mostrar Divisor]: Mostrar . 5. Fin. El Flujograma de este algoritmo puede verse en el Cuadro que hemos llamado Flujograma 10.9. Y el cdigo tiene un aspecto como el que se muestra a continuacin: #include <stdio.h> #include <math.h> int main(void) { long N;

Leer

Mostrar 1 . .
S S No No

C1 C2

C1

C2

Mostrar

Mostrar
F

Flujograma 10.9.: Algoritmo que busca todos los divisores de un entero.

264

Captulo 10. Estructuras de control II: Estructuras de iteracin.

long i; printf("Valor de N ... "); scanf( %ld", &N);

printf("Los divisores del numero %ld son ...\n\n", N); printf("%8ld", 1); for(i = 2 ; i <= N / 2 ; i++) if(!(N % i)) printf("%8ld", i); printf("%8ld", N); return 0; } La condicin recogida en la estructura condicional if es !(N % i): es decir, N % i == 0: si esta ltima expresin es verdadera, entonces N % i ser igual a cero, es decir, ser falso: y por tanto, su negacin ser verdadero.

10.11.

Se llama nmero perfecto a aquel que es igual a la suma de todos sus divisores excepto l mismo. Por ejemplo, el nmero 6 tiene como divisores los enteros 1, 2 y 3, que, efectivamente, suman 6; el nmero 28 tiene como divisores el 1, 2, 4, 7 y 14, que de nuevo efectivamente, suman 28; el 18 tiene como divisores el 1, 2, 3, 6 y 9, que suman 21, por lo que este ltimo nmero no es perfecto; los dos primeros s lo han sido. Presentar un algoritmo que muestre los nmeros perfectos entre el 1 y un milln. Este algoritmo requiere buscar todos los divisores de un entero. Ya hemos visto ms arriba cmo se hace esto. Ahora, adems de buscar los divisores, lo que debemos hacer es sumarlos. El algoritmo queda de la siguiente manera. 1. Para Hasta 1.000.000 Repetir: 1.1. (ya tomamos sumado el primer divisor) 1.2. Para y Mientras Repetir: 1.2.1. Si Entonces: 1.2.1.1.

265

Informtica Aplicada. Programacin en Lenguaje C.

C C1 C2 C3 C4 ? ? ? ? F S C1 No

S S C3 No

C2

No S C4 No

Mostrar

Flujograma 10.10.: Nmeros perfectos menores de un milln. 1.2.2. 1.3. Si Entonces: [Mostrar Valor]: Mostrar 1.4. 2. Fin.

(es nmero perfecto)

El Flujograma de este algoritmo puede verse en el Cuadro que hemos llamado Flujograma 10.10. El cdigo que implementa este algoritmo podra ser el siguiente: #include <stdio.h> #define _LIMITE 1000000 int main(void) { long suma; long i, num; for(num = 2 ; num < _LIMITE ; num++) { for(i = 1, suma = 0 ; i <= num / 2 ; i++) if(num % i == 0) suma += i; if(num == suma) printf("En entero %hd es perfecto.\n",num); }

266

Captulo 10. Estructuras de control II: Estructuras de iteracin.

return 0; } La variable num recorre todos los enteros entre 2 y _LIMITE en busca de aquellos que sean perfectos. Esa bsqueda se realiza con el for ms externo. Para cada valor distinto de num, la variable suma, que se inicializa a cero cada vez que se comienza de nuevo a ejecutar la estructura for anidada, guarda la suma de sus divisores. Eso se realiza en el for anidado. Despus del clculo de cada suma, si su valor es el mismo que el entero inicial, entonces ese nmero ser perfecto (esa es su definicin) y as se mostrar por pantalla. La bsqueda de divisores se hace desde el 1 (que siempre interviene) hasta la mitad de num: ninguno de los divisores de num puede ser mayor que su mitad. Desde luego, se podra haber inicializado la variable suma al valor 1, y comenzar a buscar los divisores a partir del 2, porque, efectivamente, el entero 1 es divisor de todos los enteros.

10.12.

Escriba un programa que muestre por pantalla un trmino cualquiera de la serie de Fibonacci.

La serie de Fibonacci est definida de la siguiente manera: el primer y el segundo elementos son iguales a 1. A partir del tercero, cualquier otro elemento de la serie es igual a la suma de sus dos elementos anteriores. Escribir el cdigo es muy sencillo una vez se tiene el flujograma. Intente hacer el flujograma. #include <stdio.h> int main(void) { unsigned long fib1 = 1, fib2 = 1, Fib = 1; unsigned short n, i = 3;

267

Informtica Aplicada. Programacin en Lenguaje C.

printf("Elemento Fibonacci: ");

scanf( %hu",&n);

while(i <= n) { Fib = fib1 + fib2; fib1 = fib2; fib2 = Fib; i++; } printf("El trmino %hu de la serie es %lu.\n",n,Fib); return 0: }

10.13.

Escriba un programa que indique si un nmero introducido por teclado es cuadrado perfecto.

Ya sabe que un cuadrado perfecto es un entero que se puede expresar como potencia al cuadrado de otro entero; por ejemplo, 1, 4, 9, 16, 25, 36, 49, etc. Hay muchas formas de dar respuesta a esta pregunta. La construccin de un programa que resuelva esta cuestin es muy sencilla; todo el problema est en saber cmo averiguarlo, al margen de la programacin. Una posibilidad es, por ejemplo, verificar que al elevar al cuadrado su raz cuadrada, expresada como entero, obtenemos el mismo nmero de inicio: #include <stdio.h> #include <math.h> int main(void) { long a; printf("Valor de a ... "); scanf( %ld", &a);

if((long)sqrt(a) * (long)sqrt(a) == a)

268

Captulo 10. Estructuras de control II: Estructuras de iteracin.

printf("S ES CUADRADO PERFECTO\n"); else printf("NO ES CUADRADO PERFECTO\n"); return 0; } Otra forma es verificar que la expresin entera de la raz cuadrada es igual a la raz cuadrada. Simplemente cambiara la condicin del if: if((long)sqrt(a) == sqrt(a)) Otra tercera opcin sera buscar, entre los enteros menores que el valor de entrada, uno que, al multiplicarlo por s mismo, ofrece como resultado el valor de entrada: #include <stdio.h> int main(void) { long a, b; printf("Valor de a ... "); for(b = 1 ; b < a ; b++) if(b * b == a) break; if(b < a) else return 0; } Otra forma de dar salida por pantalla, utilizando una sola vez la funcin printf, sera la siguiente: printf("%s ES CUADRADO PERFECTO\n" , b < a ? S : NO); printf("S ES CUADRADO PERFECTO\n"); printf("NO ES CUADRADO PERFECTO\n"); scanf( %ld", &a);

10.14.

Escriba un programa que calcule cuntos enteros primos hay entre los nmeros introducidos por teclado (ambos inclusive). No se pregunta aqu cules son esos primos sino cuntos hay en total en ese intervalo.

269

Informtica Aplicada. Programacin en Lenguaje C.

Ya hemos hecho algunos ejercicios trabajando con nmeros primos. Ya sabemos cmo se determinan. Ahora simplemente hemos de recorrer todos los enteros del intervalo y aumentar un contador cada vez que hallemos un nuevo entero primo. Una posible solucin al problema planteado podra ser la siguiente:

#include <stdio.h> #include <math.h> int main(void) { long a, b, N; short div, primos; printf("Intervalo. Primer valor.... "); scanf( %ld", &a); printf("Intervalo. Segundo valor... "); scanf( %ld", &b); if(a > b) { a ^= b ; b ^= a ; a ^= b; }

for(N = a ; N < b ; N++) { for(div = 2 ; div <= sqrt(N) ; div++) if(N % div == 0) break; if(div > sqrt(N)) primos++; } printf("%hd pr. entre %ld y %ld",primos,a,b); return 0; } El bloque de sentencias condicionado con el primer if, situado

inmediatamente despus de la entrada de datos, verifica que el intervalo est marcado por dos enteros donde el primero es menor o igual que el segundo: si no es as, intercambia esos valores que marcan los lmites del intervalo.

10.15.

Escriba un programa que sume los nmeros primos menores que mil.

270

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Ya sabemos manejar los nmeros primos. Ahora slo hay que sumarlos. Una posible solucin podra ser la siguiente: #include <stdio.h> #include <math.h> #define LIMITE 1000 int main(void) { long suma = 3; long N; short d; short raiz;

// Suma de primos: 1 + 2 + ... // Nmero candidato a primo // sucesivos posibles divisores...

for(N = 3 ; N < LIMITE ; N += 2) { for(raiz = sqrt(N), d = 3 ; d <= raiz ; d += 2) if(N % d == 0) break; if(d > raiz) suma += N; } printf("La suma es %ld", suma); return 0; }

10.16.

Escriba un programa que solicite al usuario valores enteros, uno tras otro, hasta que introduzca uno que sea Primo.

Hemos visto ya varias veces cmo sacer si un entero es o no es primo. Pero ahora hay que incluir este procedimiento dentro de otra iteracin que vaya solicitando enteros hasta llegar a uno que sea primo. Un posible cdigo podra ser el siguiente: #include <stdio.h> #include <math.h> int main(void) { long a; short d; do { printf("Valor de a ... "); scanf( %ld", &a);

271

Informtica Aplicada. Programacin en Lenguaje C.

for(d = 2 ; d <= sqrt(a) ; d++) if(a % d == 0) break; }while(d <= sqrt(a) || !a); printf("Bien: %ld es PRIMO.", a); return 0; } O, de una forma ms sencilla: #include <stdio.h> #include <math.h> int main(void) { long a; short d; do { printf("Valor de a ... "); scanf( %ld", &a); for(d = 2 ; d <= sqrt(a) ; d++) if(a % d == 0) return 0; }while(1); }

10.17.

Muestre por pantalla todos los enteros primos comprendidos entre dos enteros introducidos por teclado.

Un posible cdigo para este enunciado podra ser el siguiente: #include <stdio.h> #include <math.h> int main(void) { long a, b, div; long num; printf("Lmite inferior ... "); printf("Lmite superior ... ");

scanf( %ld",&a); scanf( %ld",&b);

printf("Los primos entre %ld y %ld son ...\n\t", a, b); for(num = a, primos = 0 ; num <= b ; num++) { for(div = 2 ; div < sqrt(num) ; div++) if(num % div == 0) break;

272

Captulo 10. Estructuras de control II: Estructuras de iteracin.

if(num % div == 0) continue; if(primos != 0 && primos % 10 == 0) printf("\n\t"); primos++; printf("%6ld,",num); } return 0; } El primer for recorre todos los enteros comprendidos entre los dos lmites introducidos por teclado. El segundo for averigua si la variable num codifica en cada iteracin del primer for un entero primo o compuesto: si al salir del segundo for se tiene que num % div es igual a cero, entonces num es compuesto y se ejecuta las sentencia continue que vuelve a la siguiente iteracin del primer for. En caso contrario, el valor de num es primo, y entonces sigue adelante con las sentencias del primer for, que estn destinadas nicamente a mostrar por pantalla, de forma ordenada, ese entero primo, al igual que habr mostrado previamente todos los otros valores primos y mostrar los que siga encontrando posteriormente. La salida por pantalla del programa podra ser la siguiente: Lmite inferior ... 123 Lmite superior ... 264 Los primos entre 123 y 264 son ... 127, 179, 233, 131, 181, 239, 137, 191, 241, 139, 193, 251, 149, 197, 257, 151, 199, 263, 157, 211, 163, 223, 167, 227, 173, 229,

Para este ltimo programa se recomienda que se dibuje el diagrama de flujo.

10.18.

Escriba un programa que calcule el nmero este nmero verifica la siguiente relacin:

,sabiendo que

273

Informtica Aplicada. Programacin en Lenguaje C.

En el captulo 5 ha quedado ya recogido este enunciado. Recogemos aqu un posible cdigo que lo resuelve: #include <stdio.h> #define LIMITE 100000 int main(void) { double PI = 0; for(int i = 1 , e = 4 ; i < LIMITE ; i += 2 , e = -e) PI += e / (double)i; printf("El valor de PI es ... %lf.",PI); return 0; } Que ofrece la siguiente salida por pantalla: El valor de PI es ... 3.141573. La variable PI se inicializa a cero. En cada iteracin ir almacenando la suma de todos los valores calculados. En lugar de calcular , calculamos directamente el valor de : por eso el numerador (variable que hemos llamado e) no vara entre -1 y +1, sino entre -4 y +4. El valor de la variable i aumenta de dos en dos, y va tomando los diferentes valores del denominador 2k + 1. Es necesario forzar el tipo de la variable i a double, para que el resultado de la operacin cociente no sea un entero, que a partir de i igual a cero dara como resultado el valor cero.

10.19.

Escriba un programa que calcule el nmero este nmero verifica la siguiente relacin:

, sabiendo que

274

Captulo 10. Estructuras de control II: Estructuras de iteracin.

De nuevo el clculo del valor del nmero pi. Estos ejercicios son muy sencillos de buscar (Internet est llena de definiciones de propiedades del nmero pi) y siempre es fcil comprobar si hemos realizado un buen cdigo: basta ejecutarlo y comprobar si sale el famoso 3.14. En esta ocasin, el cdigo podra tomar la siguiente forma: #include <stdio.h> #define LIMITE 100000 int main(void) { double PI = 2; long num = 2, den = 1, i = 1; for( ; i < LIMITE ; i++ , i % 2 ? num+=2 : den+=2) PI *= num / (double)den; printf("El valor de PI es ... %lf.",PI); return 0; }

10.20.

Defina un algoritmo que recibe por teclado las calificaciones de cada uno de los alumnos de una clase. Las notas deber ser siempre comprendidas entre el cero y el diez. Cualquiera otra nota no la tomar en consideracin. La introduccin de datos terminar cuando se introduzca la nota menos 1 (-1). Al terminar la introduccin de datos, el programa debe mostrar por pantalla la siguiente informacin: Nmero de aprobados (nota igual o mayor que cinco) y porcentaje del total de evaluados. Nmero de suspensos y porcentaje del total de evaluados. Nota media entre los aprobados. Nota media entre los suspendidos.

Un posible algoritmo de respuesta al problema podra ser el siguiente:

275

Informtica Aplicada. Programacin en Lenguaje C.

1. [Inicializar las variables]: , , 2. Repetir: 2.1. [Leer entrada]: Leer . 2.2. Si C1: ( ) Entonces: 2.2.1. . 2.2.2. . Si no, Entonces: 2.2.3. Si C2: ( ) Entonces 2.2.3.1. . 2.2.3.2. . Mientras que C3: ( ). 3. [Mostrar Resultados] 3.1. [Nmero de aprobados]: Mostrar . 3.2. [Nmero de aprobados]: Mostrar .

Mostrar

Mostrar Leer C4 Mostrar

C1

Mostrar C2

C5 Mostrar C3 C6 Mostrar Flujograma 10.11.: Correspondiente al enunciado n. 10.20. Las leyendas C1, , C6, explicitadas en el algoritmo previo.

276

Captulo 10. Estructuras de control II: Estructuras de iteracin.

3.3. Si C4: ( ) Entonces: 3.3.1. [Porcentaje aprobados]: Mostrar 3.3.2. [Porcentaje aprobados]: Mostrar 3.4. Si C5: ( Entonces [Media aprobados]: Mostrar . 3.5. Si C6: ( ) Entonces [Media aprobados]: Mostrar . 4. Fin. Donde hemos llamado: aprobados;

. .

la variable que cuenta el nmero de a la a la que suma las

a la que cuenta el nmero de suspensos;

variable que suma las notas de los aprobados; y notas de los suspensos.

El Flujograma de este algoritmo puede verse en el Cuadro que hemos llamado Flujograma 10.11. Queda pendiente que redacte el programa en lenguaje C.

10.21.

Calcule el valor del nmero e. sabiendo que verifica la siguiente relacin:

1 1 1 1 ... 0! 1! 2! 3!

El algoritmo para el clculo del nmero presentada podra ser el siguiente: 1. [Inicializar variables]: , 2. Para Hasta Repetir: 2.1. . 2.2. . 2.3. . 3. [Mostrar resultado]: Mostrar 4. Fin. La variable sumandos, el ,

a partir de la propiedad

la inicializamos a 1: ya le hemos sumado el primero de los .

277

Informtica Aplicada. Programacin en Lenguaje C.

No

Mostrar

Flujograma 10.12.: Clculo del nmero .

La variable

almacena el valor del inverso de los factoriales. Lo . el proceso de sumar

inicializamos al valor de

Se repite tantas veces como indique la variable elementos del sumatorio. La variable variable . La variable

va guardando esa suma realizada .

paso a paso segn la estructura de control iterativa gobernada por la almacena los sucesivos valores

El flujograma de este algoritmo queda recogido en Flujograma 10.12. El cdigo podra ser el siguiente: #include <stdio.h> int main(void) { double p = 1, e = 1; short n; for(n = 1 ; n < 100 ; n++) { p *= 1.0 / n; e += p; } printf("El numero e es ... %20.17lf.",e); return 0; }

10.22.

Poblacin de conejos. Supongamos que tenemos una pareja de conejos (macho y hembra) recin nacidos. Y supongamos que

278

Captulo 10. Estructuras de control II: Estructuras de iteracin.

esos conejos llegan al estado de adultos una vez transcurrido un mes de vida. Supongamos que una coneja slo puede concebir si es adulta. Supongamos tambin que toda hembra adulta pare una nueva pareja de conejos (un macho y una hembra) despus de un mes de gestacin, y que en cuanto una coneja pare sus cras, la pareja logra de inmediato volver a concebir una nueva pareja. Supongamos que en nuestra poblacin los conejos nunca se mueren; que en cada nueva cra nacen nicamente dos conejos: uno macho y otro hembra; que todos los conejos, despus del primer mes de crecimiento y a partir del segundo mes, ya puede aparearse con la correspondiente coneja y todos los meses cada pareja adulta (mayor de un mes de vida) cra dos nuevos conejillos. Escriba el algoritmo del programa que calcule la poblacin de conejos despus de transcurrir tantos meses como indique el usuario por consola.

En el primer mes nuestra poblacin de conejos est formada por un macho y una hembra no adultos; es decir, una pareja. Ni mueren, ni pueden concebir. En el segundo mes nuestra poblacin de conejos est formada por un macho y una hembra adultos; es decir, una pareja. La hembra ha concebido un par de conejos, pero stos an no han nacido. En el tercer mes tenemos dos pares de conejos: uno de conejos adultos con la coneja de nuevo en estado de buena esperanza, y una pareja de jvenes conejos. En la tabla 10.2. vemos la evolucin de las parejas de conejos mes a mes.

279

Informtica Aplicada. Programacin en Lenguaje C.

En realidad, estamos ante la serie de Fibonacci. Pero vamos a hacer como si no lo supiramos. El algoritmo se presta mucho a la recurrencia. El nmero de conejos jvenes de cada mes es el nmero de adultos de la vez anterior. Y el nmero de adultos es la suma de los jvenes y adultos del mes anterior. mes 1 2 3 4 5 6 7 8 9 10 nuevos 1 0 1 1 2 3 5 8 13 21 adultos 0 1 1 2 3 5 8 13 21 34 total 1 1 2 3 5 8 13 21 34 55

Tabla 10.2.: Evolucin de las parejas de conejos por meses. El algoritmo simple de este proceso podra ser el siguiente: 1. [Inicializar Variables]: 2. [Introducir mes]: Leer 3. Para Hasta . C . . C1 . . . . . F Flujograma 10.13.: El problema de los conejos. S No Mostrar . . . , . Repetir: , .

Leer

C1

280

Captulo 10. Estructuras de control II: Estructuras de iteracin.

[Operaciones] 3.1. . 3.2. . 3.3. . 3.4. . 3.5. . 4. [Mostrar Resultados]: Mostrar 5. Fin.

El Flujograma del algoritmo queda recogido en Flujograma 10.213.

10.23.

El nmero ureo ( ) verifica muchas curiosas propiedades. Por ejemplo:

y otras La penltima expresin presentada ( camino curioso para el clculo del nmero ureo: ) muestra un

Y, entonces un modo de calcular el nmero ureo es el siguiente:

Es decir, inicializando veces que .

al valor 1, se puede ir afinando en el

clculo del valor del nmero ureo a base de repetir muchas

Presente el algoritmo para calcular el nmero ureo mediante este procedimiento haciendo, por ejemplo, la sustitucin

281

Informtica Aplicada. Programacin en Lenguaje C.

mil veces. Mostrar luego el resultado por pantalla.

El enunciado es algo aparatoso. Pero el algoritmo que lo resuelve es muy sencillo. Hay que inicializar la variable veces la sustitucin a 1 y luego realiza 1000 . El algoritmo queda como sigue:

1. [Inicializar Variables]: . 2. Para Hasta 1000 Repetir: 2.1. . 3. [Mostrar Resultados]: Mostrar . 4. Fin. Siguiendo con el mismo enunciado, tambin podemos plantearnos calcular el nmero ureo a partir de la relacin tenemos: . De nuevo

eso nos brinda otro algoritmo, casi idntico al anterior, donde ahora cambiamos la operacin cociente por la operacin raz cuadrada. 1. [Inicializar Variables]: . 2. Para Hasta 1000 Repetir: 2.1. 3. [Mostrar Resultados]: Mostrar . 4. Fin.

282

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Los flujogramas de ambos algoritmos quedan recogidos en el cuadro Fujograma 10.14. Si Expresin es 1 1 , entonces estamos en el primer algoritmo; si Expresin es 1 , entonces estamos en el segundo algoritmo. De todas formas, estos dos clculos tienen tambin una solucin mediante concurrencia. En las expresiones que hemos tomado, el valor del nmero ureo depende del valor del nmero ureo. El algoritmo de la funcin recurrente podra tener la siguiente forma: Funcin Aureo( Real, Constantes: . Variables: . Acciones: 1. Si Entonces Sino Entonces 2. Fin. Entero) Real

El cdigo en C podra tener el siguiente aspecto: #include <stdio.h> #define LIMITE 1000 int main(void) { double au = 1; int i; for(i = 0 ; i < LIMITE ; i++) au = 1 + 1 / au;

printf("El nmero ureo es ..... %lf.\n",au); C C1 C1 Expresin Mostrar

F Flujograma 10.14.: Nmero ureo.

283

Informtica Aplicada. Programacin en Lenguaje C.

printf("ureo al cuadrado es ... %lf.\n",au * au); return 0; } No se ha hecho otra cosa que considerar lo que sugiere el enunciado: inicializar el nmero ureo a 1, y repetir mil veces la iteracin

1 1 . Al final mostramos tambin el cuadrado del nmero ureo:


es un modo rpido de verificar que, efectivamente, el nmero hallado es el nmero buscado: el nmero ureo verifica que su cuadrado es igual al nmero incrementado en uno. La salida que ofrece este cdigo por pantalla es la siguiente: El nmero ureo es ..... 1.618034. ureo al cuadrado es ... 2.618034. Con la expresin de las races cuadradas sucesivas, el cdigo queda de la siguiente manera: #include <stdio.h> #include <math.h> #define LIMITE 100000 int main(void) { double au = 1; int i; for(i = 0 ; i < LIMITE ; i++) au = sqrt(1 + au);

printf("El nmero ureo es ..... %lf.\n",au); printf("ureo al cuadrado es ... %lf.\n",au * au); return 0; } Este programa ofrece una salida idntica a la anterior. Y el cdigo es casi igual que el anterior: nicamente cambia la definicin de la iteracin.

10.24.

La funcin

( es una variable real que guarda el valor del

ngulo en radianes) puede ser expresada, utilizando sus series

284

Captulo 10. Estructuras de control II: Estructuras de iteracin.

de Taylor alrededor de cero as:

Escriba un programa que solicite del usuario el valor de

muestre por pantalla su seno, calculado de acuerdo con la expresin arriba indicada. (iterar hasta una potencia de 10.000) Usar la funcin double pow(double, double); (1er. parmetro: base; 2do. parmetro: exponente) recogida en math.h.

Una posible solucin a este problema podra ser la siguiente: #include <stdio.h> #include <math.h> #define PI 3.141596 int main(void) { double x; double seno = 0, invF = 1.0; long i, sw; printf("Valor de x ... "); scanf( %lf", &x); x *= PI / 180; // Pasamos a radianes. for(i = 1 ,sw = 1 ; i < 10000 ; sw = -sw , i += 2) { seno += sw * pow(x, i) * invF; invF *= 1.0 /((i + 1) * (i + 2)); } printf("El seno de %lf es %lf\n", x, seno); return 0; }

10.25.

Escriba un programa que muestre por pantalla todas las ternas de tres enteros menores que 100 que verifiquen la relacin del teorema de Pitgoras. Por ejemplo: 3 - 4 - 5; 5 - 12 - 13; 6 - 8

285

Informtica Aplicada. Programacin en Lenguaje C.

- 10 ; 7 - 24 - 25;

Una posible solucin a este problema planteado podra ser la siguiente: #include <stdio.h> int main(void) { long a, b, c; for(a = 1 ; a < 100 ; a++) for(b = a ; b < 100 ; b++) for(c = b ; c < 100 ; c++) if(c * c == a * a + b * b) printf("%ld-%ld-%ld\n", a, b, c); printf("\n\nFIN"); return 0; }

10.26.

Mire la integral siguiente, ya resuelta:

Para realizar un clculo aproximado de esta integral de forma analgica, bastara realizar las sucesivas sumas de los rectngulos (tan estrechos como podamos) que van desde el eje de las abscisas hasta la grfica y x . Se podra dividir el intervalo de 0 a 10 en cientos o miles de segmentos; calcular de cada uno de ellos la altura de la imagen del punto medio del segmento Realice un programa que calcule la integral de la funcin en el intervalo comprendido entre 0 y 10. Calcule la superficie de los rectngulos en intervalos de milsimas: mil intervalos entre cada entero.

286

Captulo 10. Estructuras de control II: Estructuras de iteracin.

De nuevo estamos ante un enunciado que exige un programa sencillo. La dificultad, de nuevo, no est en la sintaxis de C, sino en saber con certeza qu es lo que vamos a querer expresar. Una posible solucin ante este enunciado podra ser la siguiente: #include <stdio.h> #define TRAMOS 1000 #define RANGO 10 int main(void) { double Sup = 0; long i; for(i = 1 ; i <= RANGO * TRAMOS ; i++) Sup += (double)i / (TRAMOS * TRAMOS); printf("La superficie es ... %lf", Sup); return 0; } // Intervalos por unidad // Lmite superior de la integral

10.27.

Decimos que dos nmeros enteros positivos a y b son nmeros amigos si se verifica que a es la suma de todos los divisores de b (excepto el mismo valor de b) y b es la suma de los divisores de a (excepto el mismo valor de a). Un ejemplo es el par (220, 284), ya que: los divisores propios de 220 son 1, 2, 4, 5, 10, 11, 20, 22, 44, 55 y 110, que suman 284; y los divisores propios de 284 son 1, 2, 4, 71 y 142, que suman 220. Haga un programa que determine, cules de los enteros del intervalo [2,10000] tienen un entero amigo.

287

Informtica Aplicada. Programacin en Lenguaje C.

De nuevo trabajamos con un algoritmo que ya hemos implementado: la bsqueda de los divisores de un entero. Pero ahora esa bsqueda slo es parte de un proceso ms amplio. Debemos, para cada candidato del intervalo, buscar sus divisores y sumarlos; y con la suma obtenida hacer de nuevo el clculo de la suma de sus divisores, para verificar si esa suma es igual al entero candidato que tenamos inicialmente. Explicado de otra manera: Dentro del intervalo indicado , se calcula la suma de los divisores propios de cada i (s1); para el valor de cada suma obtenida se calcula la suma de sus correspondientes divisores propios (s2); cuando el valor de i coincide con esta segunda suma (i es igual a s2) tenemos un par de enteros amigos. Un posible cdigo podra ser el siguiente: #include <stdio.h> int main(void) { short i, d, s1, s2; for(i = 2 ; i < 10000 ; i++) { for(d = 1 , s1 = 0 ; d <= i / 2 ; d++) if(i % d == 0) s1 += d; for(d = 1 , s2 = 0 ; d <= s1 / 2 ; d++) if(s1 % d == 0) s2 += d; if(s2 == i && i != s1 && i < s1) printf("%5hd y %5hd.\n", i, s1); } return 0; } La condicin final elimina, de entre los posibles pares amigos, aquellos que son nmeros perfectos (la suma de sus divisores es igual a l mismo), y elimina las redundancias: 220 es amigo de 284 y 284 es amigo de 220.

10.28.

Cinco marineros llegan, tras un naufragio, a una isla desierta con un gran nmero de cocoteros y un pequeo mono. Dedican

288

Captulo 10. Estructuras de control II: Estructuras de iteracin.

el primer da a recolectar cocos, pero ejecutan con tanto afn este trabajo que acaban agotados, por lo que deciden repartirse los cocos al da siguiente. Durante la noche un marinero se despierta y, desconfiando de sus compaeros, decide tomar su parte. Para ello, divide el montn de cocos en cinco partes iguales, sobrndole un coco, que regala al mono. Una vez calculada su parte la esconde y se vuelve a acostar. Un poco ms tarde otro marinero tambin se despierta y vuelve a repetir la operacin, sobrndole tambin un coco que regala al mono. En el resto de la noche sucede lo mismo con los otros tres marineros. Al levantarse por la maana procedieron a repartirse los cocos que quedaban entre ellos cinco, no sobrando ahora ninguno. Cuntos cocos haban recogido inicialmente? Mostrar todas las soluciones posibles menores de 1 milln de cocos.

Este programa es sencillo de implementar. Pero hay que saber resolver el problema. Es un ejemplo de cmo saber un lenguaje no lo es todo en programacin; ms bien podramos decir que saber un lenguaje es lo de menos: lo importante es saber qu decir. Vamos a presentar la solucin ms general: Supondremos que tenemos marineros y que, cada vez que uno de ellos se levanta durante la noche para hacer aqu mostramos. Este problema tiene poco de informtica y bastante de pensar cmo dar con la solucin. Su programacin es sencilla una vez se ha logrado saber cmo dar con la respuesta. Por eso, sera til que quien quiera partes sobran cocos que se le dan al mono. As, el caso del enunciado no es ms que un caso particular de la solucin que

289

Informtica Aplicada. Programacin en Lenguaje C.

aprender a programar una aplicacin que resuelva este ejercicio, antes lo intentara resolver en un papel. El nmero de cocos (lo llamaremos primer marinero ocurra que haga mono. Es decir, es tal que ) debe ser tal que al llegar el cocos para el .

montones y sobren

Cuando el primer marinero se acuesta de nuevo, se ha llevado y escondido una porcin de los cocos. Adems ha entregado Por lo tanto, los cocos que quedan son . al mono.

Ahora se levanta el segundo marinero. Y se repite la faena: Vuelve a hacer montones, vuelven a sobrar cocos, que se los da al mono, y cocos, donde ahora es el se lleva su parte, dejando los restantes en el menguado montn inicial. Quedan, por tanto, total de cocos recolectado. Y le toca ahora el turno al tercer marinero. Y vuelve a hacer montones, vuelven a sobrar inicial. Quedan, por tanto, nmero total de cocos recolectado. Y as, van desfilando todos los marineros. Y todos realizan la misma operacin. Y siempre ocurre que sobran, despus de hacer los cocos para el mono. marineros, se hace de da, y todos montones, y esta montones, cocos, que se los da al mono, y se lleva cocos, donde ahora su parte, dejando los restantes en el cada vez ms menguado montn es el nmero de cocos que ha dejado el marinero segundo, y no el nmero de cocos que ha dejado el marinero primero, y no el nmero

Al final, cuando ya han pasado los

se levantan como si tal cosa, y reparten los cocos en veces en que se verifica que valor de de acuerdo con la expresin . cumple que

vez el mono se queda sin coco alguno. Es decir, al final, despus de , y despus de menguar el , ahora se

Vamos a expresar esto con un algoritmo:

290

Captulo 10. Estructuras de control II: Estructuras de iteracin.

1. [Entrada de datos] Leer y . 2. . 3. Mientras Repetir 3.1. . 3.2. Para Hasta Repetir: 3.2.1. Si Entonces 3.2.1.1. . 3.2.1.2. . 3.2.2. Sino [Es decir, ] Entonces 3.2.2.1. [Abandonar la iteracin]: Ir a paso 3.4. 3.3. Si y Entonces 3.3.1. [Mostrar valor]: Mostrar . 3.4. . 4. Fin. La sentencia 3.3.2.1. no es un salto de los prohibidos. Existen en muchos lenguajes (y C y Java entre ellos) sentencias de salto que se emplean para abandonar un proceso iterativo y saltar a la siguiente instruccin despus de la iteracin. Esta sentencia de salto nos saca de la iteracin controlada por la variable y nos deja en la siguiente

Leer y C1

C1 C2 C4

F y y

C2 C3 Mostrar

Flujograma 10.15.: El problema de los marineros y de los cocos.

291

Informtica Aplicada. Programacin en Lenguaje C.

instruccin o sentencia: la 3.4. En esa sentencia se evalan dos cosas: primero si ; y luego si . La segunda evaluacin es la ltima condicin que se debe verificar para el nmero de cocos: despus de haber pasado todos los marineros durante la noche, todos se renen por la maana y, a la hora de hacer los no sobra ningn coco. Para la primera condicin, conviene ver el funcionamiento del algoritmo. La variable estar a un valor igual a no se cumple que si y slo si en ningn , entonces se momento ha pasado por la sentencia 3.3.2.1. Si en alguno de los sucesivos valores de abandona la iteracin controlada por la porque el valor actual de cocos montones, tenemos ahora que

que se est evaluando no ha cumplido todas las condiciones exigidas y no es por tanto solucin a nuestro problema. Y al producirse ese abandono la variable no alcanza el valor . Es decir, si quiere decir que el valor de cocos ha fallado en algn momento algunas de las muchas condiciones que se le deben exigir. A una variable que, adems de controlar la ejecucin de una iteracin, de alguna manera informa del comportamiento centinela. Una forma ms reducida el algoritmo, sin la sentencia de abandono de la estructura de iteracin, es la siguiente: [Entrada de datos] Leer y . dentro del bloque iterado se la llama variable

1. . 2. Mientras Repetir 2.1. . 2.2. Para Hasta y Mientras 2.2.1. . 2.2.2. . 2.3. Si y Entonces 2.3.1. [Mostrar valor]: Mostrar 2.4. . 3. Fin.

Repetir:

ste es el algoritmo que queda representado en el Flujograma 10.15. #include <stdio.h>

292

Captulo 10. Estructuras de control II: Estructuras de iteracin.

int main(void) { unsigned long N = 6, n; unsigned long soluciones = 0; printf("Valores posibles de N ...\n\n"); while(N < 4000000) { unsigned short int i; N += 5; n = N; for(i = 0 ; i < 5 ; i++) { if((n - 1) % 5) break; n = 4 * (n - 1) / 5; } if(i == 5 && !(n % 5)) { printf("(%4lu) %-8lu",++soluciones, N); if(!(soluciones % 5)) printf("\n"); } } return 0; }

10.29.

Juego

de

las

15

cerillas:

Participan

dos

jugadores.

Inicialmente se colocan 15 cerillas sobre una mesa y cada uno de los dos jugadores toma, alternativamente 1, 2 o 3 cerillas pierde el jugador que toma la ltima cerilla. Buscar el algoritmo ganador para este juego, generalizando: inicialmente hay N cerillas y cada vez se puede tomar hasta un mximo de k cerillas. Los valores N y k son introducidos por teclado y decididos por un jugador (que ser el jugador perdedor), el otro jugador (que ser el ordenador, y que siempre debe ganar) decide quien empieza el juego.

293

Informtica Aplicada. Programacin en Lenguaje C.

El algoritmo que permite programar este juego es muy sencillo. Pero hay que saber llegar hasta l. A lo largo de la explicacin del juego llamaremos jugador G al ganador, es decir, al ordenador; y jugador P al perdedor, es decir, al usuario. Supongamos que hemos tomado un valor : es decir, en cada

jugada, el jugador puede retirar 1, 2, 3 cerillas. En la Tabla 10.3. queda recogido la cantidad de cerillas que se pueden llegar a retirar cada vez que juega un turno los jugadores G y P. Est claro, y se ve reflejado en el cuadro, que sea cual sea el nmero de cerillas que retire el primer jugador (el jugador P), el segundo (el jugador G) siempre puede retirar un nmero tal que en todos los turnos de juego siempre se retire el mismo nmero de cerillas. Entre las cantidades de cerillas que se pueden retirar en un turno hay un valor que se repite en todas las filas y columnas: el 4. se es el valor al que debe jugar el que retira cerillas en segundo lugar (que ser el jugador G). Si el primero (jugador P) ha retirado 1, entonces el ganador retira 3; si el primero retira 2 cerillas, entonces el ganador retira tambin 2. Si el primero retira 3 cerillas, entonces el ganador retira slo una. Y, en general, si tenemos cerillas, el nmero de cerillas que siempre se . Si el jugador P retira podrn retirar en un turno de jugada es ser el ganador, cerillas.

cerillas, entonces el jugador G deber retirar, si de verdad quiere

Jugador P 1 Jugador G 1 2 3 2 3 4 2 3 4 5 3 4 5 6

Tabla 10.3.: Cerillas que se retiran

294

Captulo 10. Estructuras de control II: Estructuras de iteracin.

despus de un turno de juego. Por lo tanto, la primera regla del jugador ganador es retirar las cerillas siempre despus del jugador que va a perder, y retirar siempre tantas cerillas como sean necesarias para lograr que en cada turno se retiren cerillas. Si, por ejemplo, tenemos y , entonces hemos quedado que

en cada turno el ganador lograr que se retiren 4 cerillas. En la figura 10.16. se ven las cerillas agrupadas en bloques de 4. El objetivo del juego es lograr que despus de la jugada del ganador quede sobre la mesa nicamente una cerilla. Si el jugador ganador va logrando que despus de cada turno se retiren una cantidad de cerillas mltiplo de ms TRES cerillas ms. Esa es la segunda regla del algoritmo ganador: el jugador ganador (jugador G) debe decidir quin comienza a jugar. Y lo hace de la siguiente manera: 1. Calcula el resto de dividir cerillas sea tal que 2. Si por sea mltiplo de : . . cerillas (es decir, 4 cerillas), lo que importa es que al inicio del juego sobre la mesa haya , ms una cerilla ms. Pero en nuestro juego no ocurre as: de hecho hay una cantidad mltiplo de 4

Ya hemos dicho que el objetivo es que al principio el nmero de

, entonces el jugador ganador cede gentilmente el turno al entonces

otro jugador, que ser quien comenzar la partida. Si

ser el jugador ganador quien comenzar a jugar, retirando

Figura 10.16.: Cerillas.

295

Informtica Aplicada. Programacin en Lenguaje C.

cerillas. A partir de ese momento tiene una cantidad de cerillas adecuada, y sin ms que lograr retirar en cada turno figura 10.16. se obtiene el valor llevar inevitablemente a la derrota a su contrincante. En el ejemplo de la , y entonces el jugador G lo que debe hacer, si desea ganar, es empezar retirando esas dos cerillas. Y a partir de este momento, el jugador G juega siempre detrs del jugador P, y retira siempre donde el jugador P. Quien deduce estas reglas del juego, ya tiene resuelto el algoritmo, que toma la siguiente forma: 1. [entrada de parmetros]: Leer y . 2. [Calcular]: . 3. Si Entonces: [Turno del ordenador]: 3.1. ( : cerillas que retira el ganador) 3.2. (hay que descontar esas cerillas) 3.3. [Mostrar informacin]: Mostrar y . 4. Mientras Hacer: 4.1. [Turno usuario]: 4.2. [Lectura]: Leer (cerillas que retira el perdedor) 4.3. (hay que descontar esas cerillas) 4.4. [Mostrar informacin]: Mostrar . 4.5. Si Entonces: 4.6. [Turno ordenador]: . 4.7. . 4.8. (hay que descontar esas cerillas) 4.9. [Mostrar informacin]: Mostrar y . 5. Si Entonces: [Mostrar ganador]: Mostrar Ha ganado el usuario. Sino Entonces: [Mostrar ganador]: Mostrar Ha ganado el ordenador. 6. Fin. En el algoritmo hemos utilizado una variable (que hemos llamado ) que nos indica quin est jugando en cada momento. Si usuario, es decir, el jugador P; si decir, el jugador G. El Flujograma del algoritmo queda recogido en Flujograma 10.17. , juega el , entonces juega el ordenador, es cerillas, es el nmero de cerillas que ha retirado en la ltima jugada

296

Captulo 10. Estructuras de control II: Estructuras de iteracin.

Leer y S Mostrar C1 No Mostrar

S Leer

C2

No Mostrar

C2

No Mostrar ,

C1 C2 C3

S Ganador Usuario

C3

No Ganador Ordenador

Flujograma 10.17.: Juego de las cerillas.

Un posible cdigo para este ejercicio podra ser el siguiente: #include <stdio.h> int main(void) { // Con cuntas cerillas se va a jugar. unsigned short cerillas; // Cuntas cerillas se pueden quitar cada vez. unsigned short quitar; // Cerillas que quita el ganador y el perdedor. unsigned short qp, qg; char ganador; do { printf("\n\n\nCon cuntas cerillas se va a jugar ... "); scanf( %hu",&cerillas); if(cerillas == 0) break;

297

Informtica Aplicada. Programacin en Lenguaje C.

do { printf("\Cuntas cerillas pueden quitarse de una vez... "); scanf( %hu",&quitar); if(quitar >= cerillas) printf("No pueden quitarse tantas cerillas.\n"); }while(quitar >= cerillas); qg = (cerillas - 1) % (quitar + 1); // MOSTRAR CERILLAS ... printf("\n"); for(short i = 1 ; i <= cerillas ; i++) { printf(" |"); if(!(i % 30))printf("\n\n"); } printf("\n"); // Fin de MOSTRAR CERILLAS if(qg) { printf("\nComienza la mquina..."); printf("\nMquina Retira %hu cerillas... \n", qg); cerillas -= qg; // MOSTRAR CERILLAS ... printf("\n"); for(short i = 1 ; i <= cerillas ; i++) { printf(" |"); if(!(i % 30))printf("\n\n"); } printf("\n"); } // FIN DE MOSTRAR CERILLAS else printf("\nComienza el jugador..."); while(cerillas != 1) { do { printf("\nCerillas que retira el jugador ... "); scanf( %hu",&qp); if(qp > quitar) printf("\nNo puede quitar mas de %hu.\n",quitar); }while(qp > quitar); cerillas -= qp; // MOSTRAR CERILLAS ...

298

Captulo 10. Estructuras de control II: Estructuras de iteracin.

printf("\n"); for(short i = 1 ; i <= cerillas ; i++) { printf(" |"); if(!(i % 30)) printf("\n\n"); } printf("\n"); // Fin de MOSTRAR CERILLAS if(cerillas == 1) { ganador = 'j'; break; } qg = quitar - qp + 1; printf("\nLa mquina retira %hu cerillas.\n",qg); cerillas -= qg; // MOSTRAR CERILLAS ... printf("\n"); for(short i = 1 ; i <= cerillas ; i++) { printf(" |"); if(!(i % 30))printf("\n\n"); } printf("\n"); // Fin de MOSTRAR CERILLAS if(cerillas == 1) ganador = 'm'; } if(ganador == 'j') printf("\nHa ganado el jugador..."); else if(ganador == 'm') printf("\nHe ganado yo, la mquina..."); }while(cerillas); return 0; } El programa ha quedado un poco ms largo que los anteriores. Se ha dedicado un poco de cdigo a la presentacin en el momento de la ejecucin. Ms adelante, cuando se haya visto el modo de definir funciones, el cdigo quedar visiblemente reducido. Por ejemplo, en cuatro ocasiones hemos repetido el mismo cdigo destinado a visualizar por pantalla las cerillas que quedan por retirar.

299

Informtica Aplicada. Programacin en Lenguaje C.

300

CAPTULO 11
ARRAYS NUMRICOS: VECTORES Y MATRICES.
Hasta el momento hemos trabajado con variables, declaradas una a una en la medida en que nos han sido necesarias. Pero pudiera ocurrir que necesitsemos un bloque de variables grande, por ejemplo para definir los valores de una matriz numrica, o para almacenar los distintos valores obtenidos en un proceso de clculo del que se obtienen numerosos resultados del mismo tipo. Supongamos, por ejemplo, que deseamos hacer un programa que ordene mil valores enteros: habr que declarar entonces mil variables, todas ellas del mismo tipo, y todas ellas con un nombre diferente. Es posible hacer una declaracin conjunta de un grupo de variables. Eso se realiza cuando todas esas variables son del mismo tipo. Esa es, intuitivamente, la nocin del array. Y ese es el concepto que introducimos en el presente captulo

Informtica Aplicada. Programacin en Lenguaje C.

Nocin y declaracin de array.


Un array (tambin llamado vector) es una coleccin de variables del mismo tipo todas ellas referenciadas con un nombre comn. La sintaxis para la declaracin de un vector es la siguiente: tipo nombre_vector[dimensin]; Donde tipo define el tipo de dato de todas las variables creadas, y dimensin es un literal que indica cuntas variables de ese tipo se deben crear. En ningn caso (estndares C89 y C90) est permitido introducir el valor de la dimensin mediante una variable. El compilador reserva el espacio necesario para almacenar, de forma contigua, tantas variables como indique el literal dimensin: reservar, pues, tantos bytes como requiera cada una de esas variables, multiplicado por el nmero de variables a crear. Por ejemplo, la sentencia short int mi_vector[1000]; reserva dos mil bytes de memoria consecutivos para poder codificar mil variables de tipo short. Cada una de las variables de un array tiene un comportamiento completamente independiente de las dems. Su nica relacin con todas las otras variables del array es que estn situadas todas ellas de forma correlativa en la memoria. Cada variable tiene su propio modo de ser llamada: desde nombre_vector[0] hasta nombre_vector[dimensin 1]. En el ejemplo anterior, tendremos 1000 variables que van desde mi_vector[0] hasta mi_vector[999]. C no comprueba los lmites del vector. Es responsabilidad del

programador asegurar que no se accede a otras posiciones de memoria contiguas del vector. Por ejemplo, si hacemos referencia al elemento mi_vector[1000], el compilador no dar como errneo ese nombre, aunque de hecho no exista tal variable.

302

Captulo 11. Arrays numricos: vectores y matrices.

La variable mi_vector[0] est posicionada en una direccin de memoria determinada: no sabemos a priori cul sea sta. Pero a partir de ella, todas los dems elementos del array tienen una ubicacin conocida deteminada. La variable mi_vector[1] est situada dos bytes ms adelante, porque mi_vector es un array de tipo short y por tanto mi_vector[0] ocupa 2 bytes (como todos los dems elementos del vector), y porque mi_vector[1] es consecutiva en la memoria, a mi_vector[0]. Esa sucesin de ubicaciones sigue en adelante, y la variable mi_vector[999] estar 1998 bytes por encima de la posicin de mi_vector[0]. Si hacemos referencia a la variable mi_vector[1000] entonces el compilador considera la posicin de memoria situada 2000 bytes por encima de la posicin de mi_vector[0]. Y de all tomar valor o escribir valor si as se lo indicamos. Pero realmente, en esa posicin el ordenador no tiene reservado espacio para esta variable, y no sabemos qu estaremos realmente leyendo o modificando. Este tipo de errores son muy graves y a veces no se detectan hasta despus de varias ejecuciones. Es lo que se denomina violacin de memoria. El recorrido del vector se puede hacer mediante ndices. Por ejemplo: short mi_vector[1000], i; for(i = 0 ; i < 1000 ; i++) mi_vector[i] = 0; Este cdigo recorre todo el vector e inicializa a cero todas y cada una de sus variables. Tngase cuidado, por ejemplo, con el recorrido del vector, que va desde el elemento 0 hasta el elemento dimensin 1. Un error habitual es escribir el siguiente cdigo: short mi_vector[1000], i; for(i = 0 ; i <= 1000 ; i++) mi_vector[i] = 0; // ERROR !!! Donde se har referencia a la posicin 1000 del vector, que no es vlida. Existe otro modo de inicializar los valores de un vector o array, sin necesidad de recorrerlo con un ndice. Se puede emplear para ello el

303

Informtica Aplicada. Programacin en Lenguaje C.

operador asignacin, dando, entre llaves y separados por comas, tantos valores como dimensin tenga el vector. Por ejemplo; short mi_vector[10] = {10,20,30,40,50,60,70,80,90,100}; que es equivalente al siguiente cdigo: short mi_vector[10], i; for(i = 0 ; i < 10 ; i++) mi_vector[i] = 10 * (i + 1); Cuando se inicializa un vector mediante el operador asignacin en su declaracin, como hay que introducir entre llaves tantos valores como sea la dimensin del vector creado, es redundante indicar la dimensin entre corchetes y tambin en el cardinal del conjunto de valores asignado. Por eso, se puede declarar ese vector sin especificar el nmero de variables que se deben crear. Por ejemplo: short mi_vector[] = {10,20,30,40,50,60,70,80,90,100}; Por lo dems, estas variables son exactamente iguales que todas las vistas hasta el momento. Y aunque son conceptos que no veremos hasta llegar al captulo 13, queda ya aqu dicho que todas las variables de tipo array pueden ser declaradas globales o locales, o static, o extern.

Nocin y declaracin de array de dimensin mltiple, o matrices.


Es posible definir arrays de ms de una dimensin. El comportamiento de esas variables vuelve a ser conceptualmente muy sencillo. La sintaxis de esa declaracin es la siguiente: tipo nombre_matriz[dim_1][dim_2][dim_N]; Donde los valores de las dimensiones son todos ellos literales. Por ejemplo podemos crear una matriz tres por tres: float matriz[3][3];

304

Captulo 11. Arrays numricos: vectores y matrices.

que reserva 9 bloques de cuatro bytes cada uno para poder almacenar valores tipo float. Esas variables se llaman tambin con ndices, en este caso dos ndices (uno para cada dimensin) que van desde el 0 hasta el valor de cada dimensin menos uno. Por ejemplo: long matriz[5][2], i, j; for(i = 0 ; i < 5 ; i++) for(j = 0 ; j < 2 ; j++) matriz[i][j] = 0; donde tenemos una matriz de cinco filas y dos columnas, toda ella con los valores iniciales a cero. Tambin se puede inicializar la matriz mediante el operador asignacin y llaves. En este caso se hara lo siguiente: long int matriz[5][2] = {{1,2},{3,4},{5,6},{7,8},{9,10}}; que es lo mismo que escribir long matriz[5][2], i, j, k; for(i = 0 , k = 1; i < 5 ; i++) for(j = 0 ; j < 2 ; j++) { matriz[i][j] = k; k++; } Y de nuevo hay que estar muy vigilante para no sobrepasar, al utilizar los ndices, la dimensin de la matriz. Para comprender mejor cmo se distribuyen las variables en la memoria, y el peligro de equivocarse en los ndices que recorren la matriz, veamos el siguiente programa que crea una matriz de 2 por 5 y muestra por pantalla la direccin de cada uno de los 10 elementos. Antes, sin embargo, hay que adelantar la presentacin de un operador (el operador direccin, &) que se explicar detalladamente en el captulo 16, al hablar de punteros. Este operador monario, aplicado prefijo a una variable, devuelve la posicin en memoria de esa variable. As las cosas y dada, por ejemplo, la variable x (short x = 7;), cuando el programa nombra a esa variable lo que hace es hacer referencia al valor codificado

305

Informtica Aplicada. Programacin en Lenguaje C.

en esa variable (decir x ser decir 7 mientras no cambie ese valor); y si al nombre de la variable le precede el operador & ya no estamos indicando su valor sino su posicin en memoria, que en arquitecturas de 32 bits supone una informacin de 4 bytes. Cuando queremos mostrar el valor de la variable x mediante la funcin printf, utilizamos, como sabemos, la combinacin de caracteres %hd; pero si lo que queremos es visualizar el valor de su posicin en memoria (la codificacin en 32 bits de esa posicin, mostrado en hexadecimal) entonces deberemos poner %p. Aclarado esto, veamos ahora el siguiente cdigo y una posible salida por pantalla: #include <stdio.h> int main(void) { char m[2][5]; short i, j; for(i = 0 ; i < 2 ; i++) for(j = 0 ; j < 5 ; j++) printf("&m[%hd][%hd] = %p\n",i,j,&m[i][j]); return 0; } La salida por pantalla ha sido esta: &m[0][0] &m[0][1] &m[0][2] &m[0][3] &m[0][4] &m[1][0] &m[1][1] &m[1][2] &m[1][3] &m[1][4] = = = = = = = = = = 0012FF80 0012FF81 0012FF82 0012FF83 0012FF84 0012FF85 0012FF86 0012FF87 0012FF88 0012FF89

Donde vemos que los elementos van ordenados desde el m[0][0] hasta el m[0][4], y a continuacin el m[1][0]: cuando termina la primera fila comienza la segunda fila. Si ahora, por equivocacin, escribiramos el siguiente cdigo

306

Captulo 11. Arrays numricos: vectores y matrices.

#include <stdio.h> void main(void) { char m[2][5]; short i, j; for(i = 0 ; i < 2 ; i++) for(j = 0 ; j < 6 ; j++) // ERROR !!! printf("&m[%hd][%hd] = %p\n",i,j,&m[i][j]); } (donde, como se ve, el ndice j recorre hasta el valor 5, y no slo hasta el valor 4) tendramos la siguiente salida por pantalla: &m[0][0] &m[0][1] &m[0][2] &m[0][3] &m[0][4] &m[0][5] &m[1][0] &m[1][1] &m[1][2] &m[1][3] &m[1][4] &m[1][5] = = = = = = = = = = = = 0012FF80 0012FF81 0012FF82 0012FF83 0012FF84 0012FF85 0012FF85 0012FF86 0012FF87 0012FF88 0012FF89 0012FF8A

Donde el compilador se ha tragado que la matriz tiene el elemento m[0][5] y el m[1][5]. No sabemos qu habr en la posicin de la variable no existente m[1][5]. S sabemos en cambio qu hay en la m[0][5]: si vemos la lista de la salida, tenemos que el compilador considera que la variable m[0][5] estar a continuacin de la m[0][4]. Pero por otro lado, ella sabe que la segunda fila comienza en m[1][0] y sabe dnde est ubicada. Si comparamos &m[0][5] y &m[1][0] veremos que a ambos se les supone la misma direccin. Y es que m[0][5] no ocupa lugar porque no existe. Pero cuando se escriba en el cdigo m[0][5] = 0; lo que estaremos poniendo a cero, quiz sin saberlo, es la variable m[1][0]. Conclusin: mucho cuidado con los ndices al recorrer matrices y vectores.

307

Informtica Aplicada. Programacin en Lenguaje C.

Arrays en el estndar C99.


El estndar C99 ha introducido cambios sustanciales en la declaracin de los arrays. Principalmente porque permite declarar el array de longitud variable: si C90 exiga que la dimensin del array estuviera definido mediante un valor literal entero o una expresin constante que pueda ser calculada en tiempo de compilacin, ahora C99 relaja esa exigencia o restriccin y permite que el tamao del array venga indicado mediante una expresin, no necesariamente constante, y no necesariamente evaluable en tiempo de compilacin. Esto permite declarar un array de longitud variable, o un array cuyo tamao queda determinado en tiempo de ejecucin. Por ejemplo: int main(void) { short size; printf(Dimension del array ... ); scanf( %hd, &size); double m[size]; // . . . return 0; } As, el array m tendr, en el momento de su declaracin, la dimensin que haya deseado el usuario al introducir un valor para la variable size. Evidentemente, es importante que ese valor no sea menor o igual que cero. Esta modificacin introducida por el estndar C99 es cmoda y til a la hora de declarar un nuevo array. Desde luego, el estndar C90 dispone de medios para crear arrays y matrices en tiempo de ejecucin: la memoria dinmica, que se presenta en el captulo 18 de este manual. Pero no es exactamente lo mismo. C99 tambin dispone de esa asignacin dinmica de memoria. No se va a hacer uso, en este manual, de esta forma de declaracin de arrays. Principalmente porque no es una forma permitida en C: es una

308

Captulo 11. Arrays numricos: vectores y matrices.

posible pega que se le puede sealar al estndar C99: mejora las prestaciones del C90, sin duda; pero pierde la vinculacin con la programacin del estndar del C++.

Ejercicios.

11.1.

Escriba el cdigo necesario para crear una matriz identidad (todos sus valores a cero, excepto la diagonal principal) de dimensin 3.

short identidad[3][3] = {{1,0,0},{0,1,0},{0,0,1}}; Otra forma de solventarlo: short identidad[3][3], i, j; for(i = 0 ; i < 3 ; i++) for(j = 0 ; j < 3 ; j++) identidad[i][j] = i == j ? 1 : 0; El operador ?: se present al hablar de las estructuras de control condicionales (cfr. Captulo 9). Como el lenguaje C devuelve el valor 1 cuando una expresin se evala como verdadera, hubiera bastando con que la ltima lnea del cdigo presentado fuese identidad[i][j] = i == j; Para mostrar la matriz por pantalla el cdigo es siempre ms o menos el mismo: for(i = 0 ; i < 3 ; i++) { printf("\n\n"); for(j = 0 ; j < 3 ; j++) printf("%5hd",identidad[i][j]); }

309

Informtica Aplicada. Programacin en Lenguaje C.

11.2.

Escriba un programa que solicite al usuario los valores de una matriz de tres por tres y muestre por pantalla la traspuesta de esa matriz introducida.

#include <stdio.h> int main(void) { short matriz[3][3]; short i, j; for(i = 0 ; i < 3 ; i++) for(j = 0 ; j < 3 ; j++) { printf("matriz[%hd][%hd] = ", i, j); scanf( %hd",&matriz[i][j]); } for(i = 0 ; i < 3 ; i++) { printf("\n\n"); for(j = 0 ; j < 3 ; j++) printf("%5hd",matriz[i][j]); } printf("\n\n\n"); for(i = 0 ; i < 3 ; i++) { printf("\n\n"); for(j = 0 ; j < 3 ; j++) printf("%5hd",matriz[j][i]); } return 0; } Primero muestra la matriz tal y como la ha introducido el usuario, y ms abajo muestra su traspuesta.

11.3.

Escriba un programa que solicite al usuario los valores de dos matrices de tres por tres y muestre por pantalla cada una de ellas, y las matrices suma y producto.

310

Captulo 11. Arrays numricos: vectores y matrices.

Este ejercicio no es sencillo para quien est empezando a programar y, ahora en concreto, para quien est comenzando con el manejo de vectores y de matrices. Pero en realidad no tiene ninguna dificultad conceptual, y es un ejercicio muy clsico. Puede ser un buen ejercicio para aprender a manejar con soltura la operatoria de ndices de matrices. Se propone aqu las operaciones con matrices cuadradas: es el caso ms sencillo. Ms adelante, entre los ejercicios propuestos en este mismo captulo, se le pedir que resuelva el caso ms general, de matrices no necesariamente cuadradas. Supongamos que tenemos las matrices A y B, con sus valores Cada coeficiente de la matriz suma S ser valores . y .

, para todos los

Para los elementos de la Matriz producto, P, la expresin trae un poco ms de matemtica: el elemento de los elementos de la fila B. Es decir es igual a la suma de los productos de la matriz de la matriz A y de la columna

No es este manual el lugar para explicar esta expresin, en principio bien conocida de todos. Con las dos operaciones claras, un posible cdigo que resuelve nuestro problema sera: #define TAM 3 #include <stdio.h> int main(void) { short a[TAM][TAM]; short b[TAM][TAM]; short s[TAM][TAM]; short p[TAM][TAM]; short i, j, k;

311

Informtica Aplicada. Programacin en Lenguaje C.

// Entrada matriz a. for(i = 0 ; i < TAM ; i++) for(j = 0 ; j < TAM ; j++) { printf("a[%hd][%hd] = ", i, j); scanf( %hd",&a[i][j]); } // Entrada matriz b. for(i = 0 ; i < TAM ; i++) for(j = 0 ; j < TAM ; j++) { printf("b[%hd][%hd] = ", i, j); scanf( %hd",&b[i][j]); } // Clculo Suma. for(i = 0 ; i < TAM ; i++) for(j = 0 ; j < TAM ; j++) s[i][j] = a[i][j] + b[i][j]; // Clculo Producto. // p[i][j]=a[i][0]*b[0][j]+a[i][1]*b[1][j]+a[i][2]*b[2][j] for(i = 0 ; i < TAM ; i++) for(j = 0 ; j < TAM ; j++) { p[i][j] = 0; for(k = 0 ; k < TAM ; k++) p[i][j] += a[i][k] * b[k][j]; } // Mostrar resultados. // SUMA for(i = 0 ; i < TAM ; i++) { printf("\n\n"); for(j = 0 ; j < TAM ; j++) printf("%4hd",a[i][j]); printf("\t"); for(j = 0 ; j < TAM ; j++) printf("%4hd",b[i][j]); printf("\t"); for(j = 0 ; j < TAM ; j++) printf("%4hd",s[i][j]); printf("\t"); } // PRODUCTO printf("\n\n\n");

312

Captulo 11. Arrays numricos: vectores y matrices.

for(i = 0 ; i < TAM ; i++) { printf("\n\n"); for(j = 0 ; j < TAM ; j++) printf("%4hd",a[i][j]); printf("\t"); for(j = 0 ; j < TAM ; j++) printf("%4hd",b[i][j]); printf("\t"); for(j = 0 ; j < TAM ; j++) printf("%4hd",p[i][j]); printf("\t"); } return 0; } En el manejo de matrices y vectores es frecuente utilizar siempre, como estructura de control de iteracin, la opcin for. Y es que tiene una sintaxis que permite manipular muy bien las iteraciones que tienen un nmero prefijado de repeticiones. Ms, en el caso de las matrices, que las mismas variables de control de la estructura son las que sirven para los ndices que recorre la matriz.

11.4.

Escriba un programa que solicite al usuario un conjunto de valores (tantos como quiera el usuario) y que al final, ordene esos valores de menor a mayor. El usuario termina su entrada de datos cuando introduzca el cero.

#include <stdio.h> int main(void) { short datos[1000]; short i, j, nn; // Introduccin de datos. i = 0; do { printf("Entada de nuevo dato ... "); scanf( %hi",&datos[i]);

313

Informtica Aplicada. Programacin en Lenguaje C.

i++; }while(datos[i - 1] != 0 && i < 1000); nn = i - 1; // Total de datos vlidos introducidos. // Ordenar datos for(i = 0 ; i <= nn ; i++) for(j = i + 1 ; j <= nn ; j++) if(datos[i] > datos[j]) { datos[i] ^= datos[j]; datos[j] ^= datos[i]; datos[i] ^= datos[j]; } // Mostrar datos ordenados por pantalla printf("\n\n"); for(i = 0 ; i <= nn ; i++) printf("%li < ", datos[i]); printf("\b\b "); return 0; } Introduccin de datos: va solicitando uno a uno todos los datos, mediante una estructura de control dowhile. La entrada de datos termina cuando la condicin es falsa: o cuando se haya introducido un cero o cuando se hayan introducido tantos valores como enteros se han creado en el vector. Se habrn introducido tantos datos como indique el valor de la variable i, donde hay que tener en cuenta que ha sufrido un incremento tambin cuando se ha introducido el cero, y ese ltimo valor no nos interesa. Por eso ponemos la variable nn al valor i 1. Ordenar datos: Tiene una forma parecida a la que se present para la ordenacin de cuatro enteros (en el tema de las estructuras de control condicionales: cfr. Ejercicio 9.2.), pero ahora para una cantidad desconocida para el programador (recogida en la variable nn). Por eso se deben recorrer todos los valores mediante una estructura de iteracin, y no como en el ejemplo de los cuatro valores que adems no estaban almacenados en vectores, y por lo tanto no se poda recorrer los distintos valores mediante ndices. Los datos quedan almacenados en el propio vector, de menor a mayor.

314

Captulo 11. Arrays numricos: vectores y matrices.

Mostrar datos: Se va recorriendo el vector desde el principio hasta el valor nn: esos son los elementos del vector que almacenan datos introducidos por el usuario.

11.5.

Escriba un programa que solicite al usuario un entero positivo e indique si ese nmero introducido es primo o compuesto. Adems, si el nmero es compuesto, deber guardar todos sus divisores en un array y mostrarlos por pantalla.

#include <stdio.h> #define TAM 1000 int main(void) { unsigned long int numero, mitad; unsigned long int i, div; unsigned long int D[TAM]; for(i = 0 ; i < TAM ; i++) D[i] = 0; D[0] = 1; // 1 es divisor de cualquier entero. printf("Numero que vamos a testear ... "); scanf( %lu", &numero); mitad = numero / 2; for(i = 1 , div = 2 ; div <= mitad ; div++) { if(numero % div == 0) { D[i] = div; i++; if(i == TAM) { printf("Vector mal dimensionado."); return -1;; } } } if(i < TAM) D[i] = numero; if(i == 1) printf("\n%lu es PRIMO.\n",numero); else { printf("\n%lu es COMPUESTO. ", numero); printf("Sus divisores son:\n\n");

315

Informtica Aplicada. Programacin en Lenguaje C.

for(i = 0 ; i < TAM && D[i] != 0; i++) printf("\n%lu", D[i]); } return 0; } Este programa es semejante a uno presentado en el captulo 9 sobre las estructuras de control. All se comenta el diseo de este cdigo. Ahora aadimos que, cada vez que se encuentra un divisor, se almacena en una posicin del vector D y se incrementa el ndice del vector (variable i). Se inicia el contador al valor 1 porque a la posicin 0 del vector ya se le ha asignado el valor 1. La variable i hace de centinela y de chivato. Si despus de buscar todos los divisores la variable i est al valor 1, entonces es seal de que no se ha encontrado ningn divisor distinto del 1 y del mismo nmero, y por tanto ese nmero es primo. Para la dimensin del vector se utiliza una constante definida con la directiva de procesador define. Si se desea cambiar ese valor, no ser necesario revisar todo el cdigo en busca de las referencias a los lmites de la matriz, sino que todo el cdigo est ya escrito sobre ese valor prefijado. Basta cambiar el valor definido en la directiva para que se modifiquen todas las referencias al tamao del vector.

11.6.

Escriba un programa que defina un array de short de 32 elementos, y que almacene en cada uno de ellos los sucesivos dgitos binarios de un entero largo introducido por pantalla. Luego, una vez obtenidos todos los dgitos, el programa mostrar esos dgitos.

Tambin se ha explicado anteriormente, en el Captulo 7, como obtener el binario de un entero dado. All ha quedado resuelto un cdigo en C que imprima los dgitos binarios de un entero por pantalla. Ahora el

316

Captulo 11. Arrays numricos: vectores y matrices.

nuevo ejercicio exige que ese cdigo quede almacenado en un array de 32 elementos. Un posible cdigo que da solucin a este programa podra ser el siguiente: #include <stdio.h> int main(void) { signed long N; unsigned short bits[32], i; unsigned long Test; do { printf("\n\nIntroduce un entero ... "); scanf( %li",&N); if(N == 0) break; for(i=0,Test = 0x80000000 ; Test ; Test >>=1,i++) bits[i] = Test & N ? 1 : 0; printf("\nCodificacin binaria interna ... "); for(i = 0 ; i < sizeof(long) * 8 ; i++) printf("%hu", bits[i]); }while(1); return 0; } Este cdigo permite introducir tantos enteros como quiera el usuario. Cuando el usuario introduzca el valor cero entonces se termina la ejecucin del programa. Ya qued explicado el funcionamiento de este algoritmo en un tema anterior. Ahora simplemente hemos introducido la posibilidad de que se almacenen los dgitos binarios en un array. Una posible salida por pantalla de este programa sera la siguiente: Introduce un entero ... 12 Codificacin binaria interna ... 00000000000000000000000000001100 Introduce un entero ... -12 Codificacin binaria interna ... 11111111111111111111111111110100 Introduce un entero ... 0

317

Informtica Aplicada. Programacin en Lenguaje C.

11.7.

Definimos la serie triangular como aquella en que cada elemento es igual a la suma de todos los enteros menores o iguales que su cardinal. Por ejemplo, el quinto elemento de la serie triangular es igual a .

Escriba un programa que guarde en un array los 10 primeros elementos de la serie y muestre luego esos 10 valores por pantalla.

El proceso es clculo de cada nuevo elemento, a partir del elemento anterior calculado, es muy sencillo: el elemento en la posicin serie triangular es igual al elemento en la posicin de la ms el valor .

As, el programa queda, por ejemplo, de la siguiente forma: #include <stdio.h> int main(void) { long tr[10], i; for(i = 1 , tr[0] = 1 ; i < 10 ; i++) tr[i] = tr[i - 1] + i + 1; for(i = 0 ; i < 10 ; i++) printf("%ld\n", tr[i]); return 0; }

11.8.

Escriba

un

programa

que

cree

una

matriz

cuadrada

de

dimensin 4, con todos los elementos situados en la diagonal principal y por encima de ella iguales a 1 y los restantes iguales a 0. No resuelva este ejercicio inicializando la matriz en la lnea de declaracin: utilice el recorrido de la matriz mediante dos estructuras for.

318

Captulo 11. Arrays numricos: vectores y matrices.

El ejercicio es muy sencillo: asignar a cada elemento de la matriz el valor 1 el valor 0 segn que el ndice i sea menor o igual que j, o sea mayor: #include <stdio.h> int main(void) { long m[4][4], i, j; for(i = 0 ; i < 4 ; i++) for(j = 0 ; j < 4 ; j++) m[i][j] = i <= j ? 1 : 0; return 0; }

11.9.

Escriba un programa que determine si en un array de 10 enteros los valores estn bien ordenados: el menor en la posicin cero y el mayor en la posicin 9.

Basta comprobar que cada uno de los elementos es menor o igual que su siguiente. Una posible solucin a este problema podra ser la siguiente:

#include <stdio.h> int main(void) { long a[10]; short i; // Aqu va la parte de introduccin de los valores. // Evidentemente habra que incluir esa parte del cdigo. // Determinar si los valores estn ordenados... for(i = 0 ; i < 9 ; i++) if(a[i] > a[i + 1]) break; if(i == 9) printf("ORDENADOS."); else printf("NO ORDENADOS."); return 0; }

319

Informtica Aplicada. Programacin en Lenguaje C.

11.10.

Haga un programa que realice la suma de todos los elementos de una matriz cuadrada y muestre ese valor por pantalla.

#include <stdio.h> #define TAM 5 int main(void) { long matriz[TAM][TAM]; long suma; short i, j; // Aqu va la parte de introduccin de los valores. // Evidentemente debe incluir esa parte del cdigo. // Calculo de la suma... for(suma = 0, i = 0 ; i < TAM ; i++) for(j = 0 ; j < TAM ; j++) suma += matriz[i][j]; printf("La suma es ... %ld", suma); return 0; }

11.11.

Indica la salida que, por pantalla, ofrece el siguiente cdigo

long v[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100}; short i, S; for(S = 0 , i = 1 ; i < 10 ; i += 2) S += *(v + i); printf("Valor de S ... %hd", S);

Este cdigo suma las posiciones impares del array. Sumar el valor de la posicin 1, de la posicin 3, de la posicin 5, de la posicin 7 y de la posicin 9: 20 + 40 + 60 + 80 + 100, que es igual a 300: Y se es el valor que mostrar por pantalla.

320

Captulo 11. Arrays numricos: vectores y matrices.

11.12.

Escriba un programa que calcule la suma de los valores situados en el permetro de una matriz de dimensin DIM.

#include <stdio.h> #define DIM 25 // Este valor puede cambiar. int main(void) { long m[DIM][DIM]; short i, j; long Suma; // Aqu va la parte de introduccin de los valores. // Evidentemente debe incluir esa parte del cdigo. for(Suma = 0 , i = 0 ; i < DIM ; i++) Suma += m[i][0] + m[i][DIM - 1]; for(i = 1 ; i < DIM - 1 ; i++) Suma += m[0][i] + m[DIM - 1][i]; printf("\n\nEl valor de la suma es ... %ld", Suma); return 0; } Simplemente hemos de sumar la primera ( ) columna. Y luego sumar la primera ( ) y la ltima ( ) y la ltima (

) fila. Y hay que tener la precaucin de no sumar dos veces los valores de las cuatro esquinas de la matriz.

11.13.

Indique, en cada sentencia, si es correcta o existe algn error sintctico. Si la expresin est correcta, indquelo con la palabra CORRECTO en el recuadro de su derecha; de los contrario, indique dnde est el error.

short x[] = {1, 2, 3, 4, 5}; short x[];

CORRECTO. MAL: Falta la dimensin del array.

321

Informtica Aplicada. Programacin en Lenguaje C.

short x[5] = {1, 2, 3, 4}; short x[] = {1, 2, 3, 4} short x[1] = {1}; short x[5]; x++; short x[3][2] = {{1,1,1},{2,2}}; short x[3]; for(i = 0 ; i <= 3; i++) x[i] = i; short x[3]; for(i = 0 ; i < 3 ; i++) *(x + i) = i; short x[5], *p = &x;

MAL: Falta un quinto valor en la asignacin. MAL: Falta el punto y coma final. CORRECTO. MAL: No se puede modificar el valor de . (cfr. Captulo 16) MAL: A la segunda fila le falta un elemento. MAL: Violacin de memoria: El ndice accede a una posicin del array no permitida. CORRECTO. (cfr. Captulo 16) MAL: p es puntero a short. &x es puntero a puntero a short. No son tipos compatibles. No vale la asignacin. (cfr. Captulo 16)

11.14.

Suponga que tenemos un array de cinco valores tipo double. Y que hemos asignado a cada una de esas posiciones los valores de los coeficientes de un polinomio de grado 4 ( ): En la posicin primera (posicin 0) hemos colocado el trmino independiente; en la posicin segunda (posicin 1) hemos colocado el trmino de la x; etc. Disee un programa que solicite del usuario un valor de ofrezca como salida por pantalla el valor del polinomio para esa entrada. y

El problema es sencillo: simplemente hay que utilizar una estructura de control iterativa gobernada por variable ( ), que vaya desde 0 hasta 4, y que vaya sumando los valores . Desde luego, vale la pena utilizar la funcin de clculo de potencias, definida en math.h. #include <stdio.h> #include <math.h>

322

Captulo 11. Arrays numricos: vectores y matrices.

int main(void) { double x, pol; double a[5] = {3, -1, 2, +5, -1}; short i; printf("Valor de x ... "); scanf( %lf", &x);

for(pol = 0 , i = 0 ; i < 5 ; i++) pol += pow(x, i) * a[i]; printf("El valor del poliomio es %lf", pol); return 0; }

11.15.

Escriba un programa que genere y almacene en un array de 10 elementos los 10 primeros factoriales de los enteros comprendidos entre 0 y 9.

Muy parecido al programa de crear un array que almacene en sus posiciones los valores de la serie triangular. Ahora en lugar de realizar la suma, lo que debemos realizar es el producto. Todo lo dems es igual. Es muy sencillo calcular el factorial de un entero factorial del entero asigna por definicin a 1. #include <stdio.h> int main(void) { long fac[10]; short i; for(fac[0] = 1 , i = 1 ; i < 10 ; i++) fac[i] = i * fac[i - 1]; for(i = 0 ; i < 10 ; i++) printf("Factorial de %hd: %10ld\n", i, fac[i]); return 0; } si se conoce el . El primer valor, el del factorial de cero, se

323

Informtica Aplicada. Programacin en Lenguaje C.

11.16.

Escriba un programa que defina una matriz tipo short de dimensin 8 X 8. Mediante dos estructuras for, anidada una dentro de la otra, asigne como valores iniciales unos y ceros de tal manera que no haya ningn dgito igual a los adyacentes en su fila y en su columna: 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0

Y muestre, despus, por pantalla, la matriz definida.

Hay muchas formas de resolver este sencillo problema. Una es la de jugar con el valor de los ndices para decidir si el valor deber ser 0 1. A la vista de la tabla de valores presentada en el enunciado, podemos ver que cuando la suma (i + j) es par, entonces el valor de esta posicin es 1; cuando la suma es impar, entonces el valor de esa posicin es 0. De acuerdo con este simple criterio, la solucin al problema podra ser algo parecido al siguiente cdigo: #include <stdio.h> int main(void) { short a[8][8], i, j; for(i = 0 ; i < 8 ; i++) for(j = 0 ; j < 8 ; j++) a[i][j] = (i + j) % 2 ? 0 : 1; // Impresin por pantalla de la matriz creada for(i = 0 ; i < 8 ; i++) { printf("\n"); for(j = 0 ; j < 8 ; j++) printf("%3hd", a[i][j]); } return 0; }

324

Captulo 11. Arrays numricos: vectores y matrices.

11.17.

Muestre la salida que, por pantalla, ofrece el siguiente cdigo:

#include <stdio.h> int main(void) { long a[2][8]; short i, j; for(i = 0 ; i < 2 ; i++) for(j = 0 ; j < 8 ; j++) a[i][j] = j ? 1 - a[i][j - 1] : i; for(i = 0 ; i < 2 ; i++ , printf("\n")) for(j = 0 ; j < 8 ; j++) printf("%5ld", a[i][j]); return 0; }

Este es un ejercicio muy parecido al anterior. La salida es: 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0

Para llegar a la solucin, lo mejor en estos ejercicios es seguir la traza de las distintas variables.

11.18.

Muestre la salida que, por pantalla, ofrece el siguiente cdigo:

#include <stdio.h> int main(void) { short a[10] = {2, 4, 6, 8, 0, 1, 3, 5, 7, 9}, i; for(i = 1 ; i < 10 ; i++) if(a[i] < a[i - 1]) break;

325

Informtica Aplicada. Programacin en Lenguaje C.

printf("%hd", i); return 0; } Este cdigo recorre el array a, de elemento en elemento, hasta encontrar que un valor es estrictamente menor que el anterior. En ese momento deja de recorrer el array y muestra el ndice que marca la posicin de ese primer valor que est fuera de orden. En el caso de nuestro enunciado, los cuatro primeros valores (de ndices 0, 1, 2, y 3) estn correctamente ordenados; pero al llegar al quinto elemento (el de ndice 4) se rompe en orden y se ocasiona la ejecucin de la sentencia break. El programa mostrar por pantalla el valor de ese ndice: el valor 4.

11.19.

Escriba un programa que declare un array de 1000 elementos (indique el tamao mediante una directiva define) y guarde en l los mil primeros enteros primos.

#include <stdio.h> #include <math.h> #define RANGO 1000 int main(void) { long primos[RANGO], pr; short i, j; for(primos[0] = 1 , primos[1] = 2 , i = 2, pr = 3 ; i < RANGO ; pr += 2) { for(j = 1 ; j < i && primos[j] <= sqrt(pr) ; j++) if(pr % primos[j] == 0) break; if(j == i || primos[j] > sqrt(pr)) primos[i++] = pr; }

326

Captulo 11. Arrays numricos: vectores y matrices.

return 0; } Ya han quedado ejercicios parecidos a este en otros captulos del manual. Ahora, a diferencia de lo que hacamos antes, vamos guardando todos los primos, y no simplemente los imprimimos por pantalla, como hacamos antes. As, ahora no probaremos a dividir por todos los enteros menores o iguales que la raz cuadrada del entero a verificar si es o no es primo: simplemente probamos a dividir con los primos menores o iguales que la raz cuadrada.

11.20.

Escriba un programa que declare un array de tipo double de 25 elementos. Asigne, a cada una de esas veinticinco posiciones del array, el valor del inverso del factorial del ndice de la posicin: al primer elemento el valor del inverso del factorial de 0; al segundo, el valor del inverso del factorial de 1; al tercero, el valor del inverso del factorial de 2; etc.

Muy parecido a algunos anteriores. No requiere especial explicacin. Tenga en cuenta que para calcular el factorial de un entero mayor que 13 hay que utilizar ya variables de tipo coma flotante, porque los valores de esos factoriales salen fuera del rango de valores de una variable long. Para calcular el inverso del factorial usamos la definicin:

El cdigo (muy sencillo) podr tener una apariencia como el siguiente: #include <stdio.h> int main(void) { double f[25]; short i;

327

Informtica Aplicada. Programacin en Lenguaje C.

for(i = 1, f[0] = 1 ; i < 25 ; i++) f[i] = f[i - 1] * 1.0 / i; return 0; }

11.21.

Declarar cuatro arrays de la misma dimensin. Al primero se le asignarn los valores por entrada de teclado. El segundo ser copia del primero. El tercero ser copia en orden inverso del primero: el ltimo elemento del array ser el primero de la copia, el segundo ser el penltimo, etc. El cuarto array ser la suma de los arrays segundo y tercero. Luego hay que mostrar los cuatro arrays en cuatro columnas.

Se muestra un posible resultado. #include <stdio.h> #define TAM 10 int main(void) { short a[TAM], b[TAM], c[TAM], d[TAM]; short i, j; // Introduccin de valores en la matriz a... for(i = 0 ; i < TAM ; i++) { printf("a[%2hd] --> ", i); scanf( %hd",&a[i]); } // La matriz b es copia de la matriz a... for(i = 0 ; i < TAM ; i++) b[i] = a[i]; // La matriz c es copia (en orden inverso) de la matriz a... for(i = 0 , j = TAM - 1 ; i < TAM ; i++, j--) c[i] = a[j]; // La matriz s es la suma de las matrices b y c... for(i = 0 ; i < TAM ; i++) d[i] = b[i] + c[i];

328

Captulo 11. Arrays numricos: vectores y matrices.

// Muestra de las cuatro matrices... printf(" a[i]"); printf(" b[i]"); printf(" c[i]"); printf(" d[i]\n"); for(i = 0 ; i < 4 ; i++) printf(" _________"); printf("\n"); for(i = 0 ; i < TAM ; i++) { printf("%10hd", a[i]); printf("%10hd", b[i]); printf("%10hd", c[i]); printf("%10hd\n", d[i]); } return 0; }

11.22.

Declarar un array de tipo long de 30 elementos. Y asignar a cada uno de esos elementos cada uno de los treinta primeros valores de la serie de fibonacci. La serie de fibonacci se define de la siguiente forma: los dos primeros elementos son iguales a 1; el resto son iguales a la suma de los sus dos anteriores.

Se muestra un posible resultado. #include <stdio.h> int main(void) { long fibo[30]; short i; for(i = 2 , fibo[0] = fibo[1] = 1 ; i < 30 ; i++) fibo[i] = fibo[i - 1] + fibo[i - 2]; for(i = 0 ; i < 30 ; i++) printf("fibo[%2hd] = %10ld\n", i, fibo[i]); return 0; }

329

Informtica Aplicada. Programacin en Lenguaje C.

11.23.

Cree dos matrices. Asigne como quiera valores a la primera de ellas (o por entrada de teclado o por programa) Luego asigne a la segunda matriz los valores que la definan como matriz traspuesta de la primera. Deber declarar ambas matrices con las dimensiones correctas para que, efectivamente, la segunda pueda ser la traspuesta de la primera.

Para dejar bien definidas las matrices creamos varias directrices define; y hacemos que el nmero de las columnas de B sean igual al de las filas de A, y que el nmero de las filas de B sean igual al de las columnas de A. Se muestra un posible resultado. #include <stdio.h> #define _FA 8 #define _CA 5 #define _FB _CA #define _CB _FA int main(void) { long a[_FA][_CA]; long b[_FB][_CB]; short i, j, k; for(i = 0 , k = 1 ; i < _FA ; i++) for(j = 0 ; j < _CA ; j++ , k++) a[i][j] = k; for(i = 0 ; i < _FA ; i++) for(j = 0 ; j < _CA ; j++) b[j][i] = a[i][j]; for(i = 0 ; i < _FA ; i++) { for(j = 0 ; j < _CA ; j++) printf("%8ld", a[i][j]); printf("\n\n"); } printf("\n\n"); for(i = 0 ; i < _FB ; i++) {

330

Captulo 11. Arrays numricos: vectores y matrices.

for(j = 0 ; j < _CB ; j++) printf("%8ld", b[i][j]); printf("\n\n"); } return 0; }

11.24.

Declare 3 matrices. Asigne valores como quiera a las dos primeras. Y realice el producto de matrices que deber quedar registrado en la tercera matriz. Piense primero cmo se expresa cada uno de los coeficientes de la tercera matriz a partir de los coeficientes de las dos primeras. Piense tambin en las dimensiones que debern tener las matrices para que se pueda realizar entre ellas la operacin producto y para que la tercera pueda ser, de hecho, la matriz producto de las dos primeras.

Este ejercicio ya lo hemos resuelto antes, en uno de los primeros de la serie de este captulo. Pero antes tombamos las matrices cuadradas de 3 X 3. Ahora es ms complicado. Para lograr que las dimensiones de las matrices sean las correctas, usamos directivas define, de la misma forma que hemos hecho en el ejercicio anterior. Se muestra un posible resultado. #include <stdio.h> #define #define #define #define #define #define _FA _CA _FB _CB _FC _CC 7 4 _CA 9 _FA _CB

331

Informtica Aplicada. Programacin en Lenguaje C.

int main(void) { long a[_FA][_CA]; long b[_FB][_CB]; long c[_FC][_CC]; short i, j, k; for(i = 0 , k = 1 ; i < _FA ; i++) for(j = 0 ; j < _CA ; j++ , k += 2) a[i][j] = k; for(i = 0 , k = 2 ; i < _FB ; i++) for(j = 0 ; j < _CB ; j++ , k += 3) b[i][j] = k; for(i = 0 ; i < _FC ; i++) for(j = 0 ; j < _CC ; j++) for(c[i][j] = k = 0 ; k < _CA ; k++) c[i][j] += a[i][k] * b[k][j]; // MOSTRAR MATRICES ... printf("MATRIZ A ...\n\n"); for(i = 0 ; i < _FA ; i++) { for(j = 0 ; j < _CA ; j++) printf("%8hd", a[i][j]); printf("\n\n"); } printf("MATRIZ B ...\n\n"); for(i = 0 ; i < _FB ; i++) { for(j = 0 ; j < _CB ; j++) printf("%8hd", b[i][j]); printf("\n\n"); } printf("MATRIZ C ...\n\n"); for(i = 0 ; i < _FC ; i++) { for(j = 0 ; j < _CC ; j++) printf("%8hd", c[i][j]); printf("\n\n"); } return 0; }

332

Captulo 11. Arrays numricos: vectores y matrices.

11.25.

Suponga que cada posicin de un array recoge el coeficiente de un polinomio. La posicin primera (array[0]) el trmino independiente; la segunda posicin (array[1]) la del trmino de la . La tercera posicin (array[2]) la del trmino del para . ; y en general, la de la posicin i-sima (array[i]) la del trmino

Vamos a trabajar con dos polinomios: Declare para el primero un array de varios elementos (los que quiera: 10 por ejemplo) y otro de uno ms que el anterior (por ejemplo 11). Asigne por teclado valores a cada uno de los coeficientes del polinomio representado por el primer array. El segundo polinomio deber ser el polinomio integrado del primero. El trmino independiente deber dejarlo a cero. Y entonces solicitar al usuario dos valores de intervalo marcado por el usuario. para que el programa realice el clculo de la integral definida en ese

Se muestra un posible resultado. #include <stdio.h> #include <math.h> #define GRADO 10 int main(void) { double pol[GRADO], ipol[GRADO + 1]; short i; double x1, x2, integral; for(i = 0 ; i < GRADO ; i++) { printf("Coeficiente %hd ... ", i); scanf( %lf", pol + i); } printf("Valor scanf( %lf", printf("Valor scanf( %lf", de x1 ... "); &x1); de x2 ... "); &x2);

333

Informtica Aplicada. Programacin en Lenguaje C.

// INTEGRAL: for( ipol[0] = 0 , i = 0 ; i < GRADO ; i++) ipol[i + 1] = pol[i] / (i + 1); // Calculo del valor de la integral indefinida... for(integral = 0 , i = 1 ; i <= GRADO ; i++) integral += ipol[i] * pow(x2, i); for(i = 1 ; i <= GRADO ; i++) integral -= ipol[i] * pow(x1, i); printf("\n\n\tIntegral ... %lf", integral); return 0; }

11.26.

Ahora, con el mismo planteamiento del ejercicio anterior (11.25.) resuelva el problema de la derivada: cree un nuevo polinomio b que guarde la derivada del polinomio a.

Se muestra un posible resultado. #include <stdio.h> #include <math.h> #define GRADO 10 int main(void) { double pol[GRADO], dpol[GRADO]; short i; for(i = 0 ; i < GRADO ; i++) { printf("Coeficiente %hd ... ", i); scanf( %lf", pol + i); } // DERIVADA: for(i = 0 ; i < GRADO - 1; i++) dpol[i] = pol[i + 1] * (i + 1); dpol[GRADO - 1] = 0; // Impresin de los dos polinomios... for(i = 0 ; i < GRADO ; i++) printf("%8.2lf", pol[i]); printf("\n");

334

Captulo 11. Arrays numricos: vectores y matrices.

for(i = 0 ; i < GRADO ; i++) printf("%8.2lf", dpol[i]); printf("\n"); return 0; }

11.27.

Un cuadro mgico es una matriz cuadrada de dimensin n donde todas las filas, y columnas, y las diagonales principales, suman el mismo valor. Por ejemplo: 6 7 2 1 5 9 8 3 4

La tcnica que se utiliza para generar cuadros mgicos (que tienen siempre una dimensin impar: impar nmero de filas y de columnas) es la siguiente: a. Se comienza fijando el entero 1 en el espacio central de la primera fila. b. Se van escribiendo los sucesivos nmeros (2, 3, ...) sucesivamente, en las casillas localizadas una fila arriba y una columna a la izquierda. Estos desplazamientos se realizan tratando a la matriz como si estuviera envuelta sobre s misma, de forma que moverse una posicin hacia arriba desde la fila superior lleva a la fila inferior, y moverse una posicin a la izquierda desde la primera columna lleva a la columna ms a la derecha del cuadro. c. Si se llega a una posicin ya ocupada (es decir, si arriba a la izquierda ya est ocupado con un nmero anterior), entonces la posicin a rellenar cambia, que ahora ser la inmediatamente debajo de la ltima casilla rellenada.

335

Informtica Aplicada. Programacin en Lenguaje C.

Despus se contina el proceso tal y como se ha descrito en el punto anterior. Escriba un programa que genere el cuadro mgico de la dimensin que el usuario desee, y lo muestre luego por pantalla.

Para llegar a una solucin para este programa ofrecemos el flujograma desglosado en partes. Est recogido en la figura 11.1 (ms adelante, al final de este captulo). Con l se puede implementar fcilmente el cdigo que imprima el cuadro mgico. El primer paso (que el usuario introduzca el valor de la dimensin de la matriz cuadrada) debera hacerse de tal manera que slo se acepta un valor que sea impar; en caso contrario, el programa vuelve a solicitar una dimensin: y as hasta que el usuario acierta a introducir un valor impar. Aparte del cdigo que cada uno pueda escribir de la mano del flujograma, ofrecemos ahora otro que agiliza de forma notable la bsqueda de la siguiente posicin del cuadro donde se ha de colocar el siguiente valor de numero. En el cdigo se ha definido una macro mediante la directiva #define. No es trivial verlo a la primera, pero ayuda el ejemplo a comprender que a veces un cdigo bien pensado facilita su comprensin. El cdigo es el siguiente: #include <stdio.h> #define lr(x, N) ((x)< 0 ? N+(x)%N : ((x)>=N ? (x)%N : (x) )) int main(void) { unsigned short magico[17][17]; unsigned short fil, col, dim, num; do { printf( "\nDimensin ( impar entre 3 y 17 ): "); scanf( %hu", &dim); }while(dim % 2 == 0 || dim > 17);

336

Captulo 11. Arrays numricos: vectores y matrices.

printf( "\nCuadro Mgico de dimensin %hu:\n\n", dim); //Inicializamos la matriz a cero for(fil = 0 ; fil < dim ; fil++) for(col = 0 ; col < dim ; col++) magico[fil][col] = 0; // Algoritmo de asignacin de valores... for(fil = dim/2 , col = 0 , num = 1 ; num <= dim*dim;) { if(magico[fil][col] == 0) { magico[fil][col] = num++; fil = lr(fil + 1, dim); col = lr(col - 1, dim); } else { fil = lr(fil - 1, dim); col = lr(col + 2, dim); } } // Mostramos ahora el cuadrado mgico por pantalla for(fil = 0 ; fil < dim ; fil++) { printf("\n\n"); for(col = 0 ; col < dim ; col++) printf("%5hu", magico[fil][col]); } return 0; }

337

Informtica Aplicada. Programacin en Lenguaje C.

C C C

Salto lnea Inicializar matriz Rellenar matriz Mostrar matriz F S C1 No S F F C1 No

No

C2

S S C2 No

Proceso general

Iniciar Matriz

Mostrar Matriz

S Buscar siguiente posicin (fil , col)

C1

No

Rellenar Matriz
C S

C1

No

C2

No

C3

No

Buscar siguiente posicin Figura 11.1.: Flujograma para la implementacin del cuadro mgico.

338

CAPTULO 12
CARACTERES Y CADENAS DE CARACTERES.
Ya hemos visto que un carcter se codifica en C mediante el tipo de dato char. Es una posicin de memoria (la ms pequea que se puede reservar en C: un byte) que codifica cada carcter segn un cdigo arbitrario. Uno muy extendido en el mundo del PC y en programacin con lenguaje C es el cdigo ASCII. Hasta el momento, cuando hemos hablado de los operadores de una variable char, hemos hecho referencia al uso de esa variable (que no se ha recomendado) como entero de pequeo rango o dominio. Pero donde realmente tiene uso este tipo de dato es en el manejo de caracteres y en el de vectores o arrays declarados de este tipo. A un array de tipo char se le suele llamar cadena de caracteres. En este captulo vamos a ver las operaciones bsicas que se pueden realizar con caracteres y con cadenas. No hablamos de operadores,

Informtica Aplicada. Programacin en Lenguaje C.

porque estos ya se han visto antes, en un captulo previo, y se reducen a los aritmticos, lgicos, relacionales y a nivel de bit. Hablamos de operaciones habituales cuando se manejan caracteres y sus cadenas: operaciones que estn definidas como funciones en algunas bibliotecas del ANSI C y que presentaremos en este captulo.

Operaciones con caracteres.


La biblioteca ctype.h contiene abundantes funciones para la manipulacin de caracteres. En la ayuda on line que ofrece cualquier editor de C se pueden encontrar indicaciones concretas y prcticas para el uso de cada una de ellas. La biblioteca ctype.h define funciones de manejo de caracteres de forma individual, no concatenados formando cadenas. Lo ms indicado ser ir viendo cada una de esas funciones y explicar qu operacin realiza sobre el carcter y qu valores devuelve. int isalnum(int c); Recibe el cdigo ASCII de una carcter y devuelve el valor 1 si el carcter que corresponde a ese cdigo ASCII es una letra o un dgito numrico; en caso contrario devuelve un 0. Ejemplo de uso: if(isalnum(@)) printf(Alfanumrico): else printf(No alfanumrico); As podemos saber si el carcter @ es considerado alfabtico o numrico. La respuesta ser que no lo es. Evidentemente, para hacer uso de esta funcin y de todas las que se van a ver en este epgrafe, hay que indicar al compilador la biblioteca en donde se encuentran definidas estas bibliotecas: #define <ctype.h>. int isalpha(int c);

340

Captulo 12. Caracteres y cadenas de caracteres.

Recibe el cdigo ASCII de una carcter y devuelve el valor 1 si el carcter que corresponde a ese cdigo ASCII es una letra; en caso contrario devuelve un 0. Ejemplo de uso: if(isalnum(2)) printf(Alfabtico): else printf(No alfabtico); La respuesta ser que 2 no es alfabtico. int iscntrl(int c); Recibe el cdigo ASCII de una carcter y devuelve el valor 1 si el carcter que corresponde a ese cdigo ASCII es el carcter borrado o un carcter de control (ASCII entre 0 y 1F, y el 7F, en hexadecimal); en caso contrario devuelve un 0. Ejemplo de uso: if(iscntrl(\n)) printf(Carcter de control): else printf(No carcter de control); La respuesta ser que el salto de lnea s es carcter de control. Resumidamente ya, presentamos carcter. int isdigit(int c); Devuelve el valor 1 si el carcter es un dgito; en caso contrario devuelve un 0. int isgraph(int c); Devuelve el valor 1 si el carcter es un carcter imprimible; en caso contrario devuelve un 0. int isascii(int c); Devuelve el valor 1 si el cdigo ASCII del carcter es menor de 128; en caso contrario devuelve un 0. int islower(int c); Devuelve el valor 1 si el carcter es una letra minscula; en caso contrario devuelve un 0. int ispunct(int c); Devuelve el valor 1 si el carcter es signo de puntuacin; en caso contrario devuelve un 0. el resto de funciones de esta

biblioteca. Todas ellas reciben como parmetro el cdigo ASCII de un

341

Informtica Aplicada. Programacin en Lenguaje C.

int isspace(int c); Devuelve el valor 1 si el carcter es el espacio en blanco, tabulador vertical u horizontal, retorno de carro, salto de lnea, u otro carcter de significado espacial en un texto; en caso contrario devuelve un 0.

int isupper(int c); Devuelve el valor 1 si el carcter es una letra mayscula; en caso contrario devuelve un 0.

int isxdigit(int c); Devuelve el valor 1 si el carcter es un dgito hexadecimal (del 0 al 9, de la a a la f de la A a la F); en caso contrario devuelve un 0.

int tolower(int ch); Si el carcter recibido es una letra mayscula, devuelve el ASCII de su correspondiente minscula; en caso contrario devuelve el mismo cdigo ASCII que recibi como entrada.

int toupper(int ch); Si el carcter recibido es una letra minscula, devuelve el ASCII de su correspondiente mayscula; en caso contrario devuelve el mismo cdigo ASCII que recibi como entrada. Con estas funciones definidas se pueden trabajar muy bien muchas operaciones que se pueden realizar con caracteres. Por ejemplo, veamos un programa que solicita caracteres por consola, hasta que recibe el carcter salto de lnea, y que nicamente muestra por pantalla los caracteres introducidos que sean letras. Al final indicar cuntas veces han sido pulsadas cada una de las cinco vocales. #include <stdio.h> #include <ctype.h> int main(void) { unsigned short int a = 0, char ch; do { ch = getchar(); if(isalpha(ch)) { if(ch == 'a') else if(ch == else if(ch == else if(ch ==

e = 0, i = 0, o = 0, u = 0;

a++; 'e') e++; 'i') i++; 'o') o++;

342

Captulo 12. Caracteres y cadenas de caracteres.

else if(ch == 'u') u++; } else printf("\b "); }while(ch != 10); printf("\n\nVocal printf("\nVocal e printf("\nVocal i printf("\nVocal o printf("\nVocal u return 0; } Si el carcter introducido es alfabtico, entonces simplemente verifica si es una vocal y aumenta el contador particular para cada vocal. Si no lo es, entonces imprime en pantalla un retorno de carro y un carcter blanco, de forma que borra el carcter que haba sido introducido mediante la funcin getchar y que no deseamos que salga en pantalla. El carcter intro es el ASCII 13. As lo hemos sealado en la condicin que regula la estructura dowhile. a ... %hu",a); ... %hu",e); ... %hu",i); ... %hu",o); ... %hu",u);

Entrada de caracteres.
Hemos visto dos funciones que sirven bien para la introduccin de caracteres. La funcin scanf espera la entrada de un carcter ms el carcter intro. No es muy cmoda cuando se espera nicamente un carcter. La funcin getchar tambin est definida en la biblioteca stdio.h. De todas formas, su uso no es siempre el esperado, por problemas del buffer de teclado. Un buffer es como una cola o almacn que crea y gestiona el sistema operativo. Nuestro programas nunca actan directamente sobre la mquina, sino siempre a travs de los servicios que ofrece ese sistema operativo. Con frecuencia ocurre que la funcin getchar debera esperar la entrada por teclado de un carcter, pero no

343

Informtica Aplicada. Programacin en Lenguaje C.

lo hace porque ya hay caracteres a la espera en el buffer gestionado por el sistema operativo. Si se est trabajando con un editor en el sistema operativo Windows, se puede hacer uso de la biblioteca conio.h y de algunas de sus funciones de entrada por consola. Esta biblioteca no es estndar en ANSI C, pero si vamos a trabajar en ese sistema operativo s resulta vlida. En esa biblioteca vienen definidas dos funciones tiles para la entrada por teclado, y que no dan los problemas que da, en Windows, la funcin getchar. Esas dos funciones son: int getche(void); espera del usuario un pulso de teclado. Devuelve el cdigo ASCII del carcter pulsado y muestra por pantalla ese carcter. Al ser invocada esta funcin, no recibe valor o parmetro alguno: por eso se define como de tipo void. int getch(void); espera del usuario un pulso de teclado. Devuelve el cdigo ASCII del carcter pulsado. Al ser invocada esta funcin, no recibe valor o parmetro alguno: por eso se define como de tipo void. Esta funcin no tiene eco en pantalla, y no se ve el carcter pulsado.

Cadena de caracteres.
Una cadena de caracteres es una formacin de caracteres. Es un vector tipo char, cuyo ltimo elemento es el carcter nulo ( NULL, \0 se escribe en C). Toda cadena de caracteres termina con el carcter llamado carcter NULO de C. Este carcter indica donde termia la cadena. La declaracin de una cadena de caracteres se realiza de forma similar a la de un vector de cualquier otro tipo: char mi_cadena[dimensin];

344

Captulo 12. Caracteres y cadenas de caracteres.

donde dimensin es un literal que indica el nmero de bytes que se deben reservar para la cadena (recuerde que una variable tipo char ocupa un byte). La asignacin de valores, cuando se crea una cadena, puede ser del mismo modo que la asignacin de vectores: char mi_cadena[4] = {h,o,l,a}; Y as hemos asignado a la variable mi_cadena el valor de la cadena hola. Y as, con esta operacin de asignacin, acabamos de cometer un error importante. Repasemos qu es una cadena?: es un vector tipo char, cuyo ltimo elemento es el carcter nulo. Pero si el ltimo elemento es el carcter nulo no nos hemos equivocado en algo?: S. La correcta declaracin de esta cadena sera: char mi_cadena[5] = {h,o,l,a,\0}; Faltaba el carcter nulo con el que debe terminar toda cadena. De todas formas, la asignacin de valor a una cadena suele hacerse mediante comillas dobles, de la siguiente manera: char mi_cadena[5] = hola; Y ya en la cadena hola se recoge el quinto carcter: el carcter nulo. Ya se encarga el compilador de C de introducir el carcter nulo al final de la cadena. Y es que hay que distinguir, por ejemplo, entre el carcter a y la cadena a. En el primer caso tratamos del valor ASCII 97, que codifica la letra a minscula; en el segundo caso tratamos de una cadena de dos caracteres: el carcter a seguido del carcter nulo (cuyo valor ASCII es 0). Tambin podramos haber hecho lo siguiente: char mi_cadena[100] = hola;

345

Informtica Aplicada. Programacin en Lenguaje C.

donde todos los bytes posteriores al carcter nulo tendrn valores aleatorios, no asignados. Pero eso no importa, porque la cadena slo se extiende hasta el carcter nulo: no nos interesa lo que pueda haber ms all de ese carcter. Y al igual que con los arrays, podemos inicializar la cadena, sin necesidad de dimensionarla: char mi_cadena[] = hola; Y entonces ya se encarga el compilador de reservar cinco bytes para la variable mi_cadena. Una forma de vaciar una cadena y asignarle el valor de cadena vaca es el siguiente; mi_cadena[0] = \0; donde, ya lo hemos dicho, \0 es el carcter nulo. Definimos longitud de la cadena como el nmero de caracteres previos al carcter nulo. El carcter nulo no cuenta como parte para el clculo de la longitud de la cadena. La cadena hola necesita cinco variables char para ser almacenada, pero su longitud se dice que es cuatro. Asignando al elemento de ndice 0 el valor nulo, tenemos una cadena de longitud cero. Cada elemento de la cadena se reconoce a travs del ndice entre corchetes. Cuando se quiere hacer referencia a toda la cadena en su conjunto, se emplea el nombre de la cadena sin ningn corchete ni ndice.

Dar valor a una cadena de caracteres.


Para recibir cadenas por teclado disponemos, entre otras, de la funcin scanf ya presentada en captulos anteriores. Basta indicar, entre comillas dobles, y despus del carcter % la letra s. Ya lo vimos en el

346

Captulo 12. Caracteres y cadenas de caracteres.

Captulo 8. Pero esta funcin toma la cadena introducida por teclado, y la corta a partir de la primera entrada de un carcter en blanco. Puede hacer un pequeo programa para comprobar a qu nos estamos refiriendo: #include <stdio.h> int main(void) { char cadena[100]; printf("Valor de la cadena ... "); scanf( %s", cadena); printf("Cadena introducida ... %s.\n", cadena); return 0; } Si introduce una cadena con un carcter en blanco (por ejemplo, si introduce ABCDE ABCDE ABCDE) obtendr una salida truncada desde el momento en que en la entrada encontr un primer espacio en blanco. La salida por pantalla de este programa es: Cadena introducida ... ABCDE. El motivo es que la funcin scanf toma los datos de entrada del buffer (porcin temporal de memoria: ya se explic en el Captulo 8) del archivo estndar de entrada stdin. Cuando se pulsa la techa Enter (INTRO, Return, o como quiera llamarse) la funcin scanf verifica que la informacin recibida es correcta y, si lo es, almacena esa informacin recibida en el espacio de memoria indicado (cadena, en nuestro ejemplo). Y como scanf toma las palabras como cadenas de texto, y toma los espacios en blanco como separador de entradas para distintas variables, se queda con la primera entrada (hasta llegar a un espacio en blanco) y deja el resto en el buffer. Podemos averiguar lo que hay en el buffer. Eso nos servir para comprender mejor el comportamiento de la funcin scanf y comprender tambin su relacin con el buffer. Introduzca el siguiente cdigo en su editor de programas:

347

Informtica Aplicada. Programacin en Lenguaje C.

#include <stdio.h> int main(void) { char a[100], c; printf("Introducir cadena de texto ... "); scanf( %s", a); printf("\nCadena recibida por scanf ... %s", a); printf("\nQueda en el buffer .......... "); while((c = getchar()) != '\n') printf("%c", c); return 0; } Ejecute el programa, e introduzca por teclado, por ejemplo, la entrada Programar es sencillo (introduzca el texto sin comillas, claro); y pulse la tecla intro. sta ser la salida que, por pantalla, obtendr: Cadena recibida por scanf ... Programar Queda en el buffer .......... es sencillo Se puede evitar ese corte, insertando en la cadena de control y entre corchetes algunos caracteres. Si se quiere utilizar la funcin scanf para tomar desde teclado el valor de una cadena de caracteres, sin tener problema con los caracteres en blanco, bastar poner: scanf( %[^\n]s, nombreCadena); Entre corchetes ha quedado indicado el nico carcter ante el que la funcin scanf considerar que se ha llegado al final de cadena: aqu ha quedado indicado, como carcter de fin de cadena, el de salto de lnea. Si cambiamos la lnea donde est, en el programa anterior, la funcin scanf, y ponemos lo siguiente: scanf( %[^\n]s", cadena); Y ejecutamos ahora de nuevo el programa, e introducimos la misma entrada, tendremos la siguiente salida: Cadena introducida ... ABCDE ABCDE ABCDE.

348

Captulo 12. Caracteres y cadenas de caracteres.

Y en el ltimo ejemplo propuesto, si modificamos la sentencia scanf, y la dejamos scanf( %[^\n]s", a); la salida por pantalla de nuestro programa quedar: Cadena recibida por scanf ... Programar es sencillo Queda en el buffer .......... Acabamos de introducir una herramienta sintctica que ofrece la funcin scanf para filtrar las entradas de texto. Cuando la entrada de la funcin scanf es una cadena de texto, podemos indicarlo mediante la letra de tipo %s, o mediante %[], indicando entre corchetes qu conjunto de posibles caracteres se esperan como entrada, o qu caracteres no se esperan. Veamos algunos ejemplos: scanf( %[A-Za-z], a); La cadena estar formada por los caracteres introducidos mientras stos sean letras maysculas o minsculas. Si el usuario introduce ABCDE12345ABCDE, la cadena a valdr nicamente ABCDE: en cuanto haga su aparicin el primer carcter no incluido en la lista entre corchetes se terminar la entrada de la cadena. El resto de la entrada quedar en el buffer de stdin. scanf([^A-Z], a); En este caso, la funcin scanf no terminar su ejecucin hasta que no reciba un carcter alfabtico en maysculas. Por ejemplo, podra dar como entrada la siguiente cadena: buenas tardes. este texto de prueba sigue siendo parte de la entrada. 12345: se incluyen los dgitos. FIN La cadena a valdra entonces todos los caracteres introducidos desde la b de buenas hasta el punto despus de la palabra

349

Informtica Aplicada. Programacin en Lenguaje C.

dgitos. El siguiente carcter introducido es una letra mayscula (la primera de la palabra FIN, y entonces scanf interrumpe la entrada de la cadena. Esos tres ltimos caracteres FIN, quedarn en el buffer del archivo estndar stdin. Tambin se puede limitar el nmero de caracteres de la entrada: por ejemplo: scanf( %5s, a); Si el usuario introduce la cadena ABCDEabcdeABCDE, el valor de a ser nicamente ABCDE, que son los cinco primeros caracteres introducidos. El limitador de longitud de la cadena es muy til a la hora de dar entrada a cadenas de texto, porque se evita una violacin de memoria en el caso de que la cadena de entrada sea de longitud mayor que la longitud del array tipo char que se ha declarado. Otra funcin, quiz ms cmoda (pero que no goza de tantas posibilidades de control en el valor de la cadena de entrada), es la funcin gets. La funcin gets est tambin definida en la biblioteca stdio.h, y su prototipo es el siguiente: char *gets(char *s); Esta funcin asigna a la cadena s todos los caracteres introducidos como cadena. La funcin queda a la espera de que el usuario introduzca la cadena de texto. Hasta que no se pulse la tecla intro, se supone que todo lo que se teclee ser parte de la cadena de entrada. Al final de todos ellos, como es natural, coloca el carcter nulo. Hay que hacer una advertencia grave sobre el uso de estas funciones de entrada de cadenas de texto: puede ocurrir que la cadena introducida por teclado sea de mayor longitud que el nmero de bytes que se han reservado. Esa incidencia no es vigilada por la funcin gets. Y si ocurre, entonces, adems de grabar informacin de la cadena en los bytes reservados para ello, se har uso de los bytes, inmediatamente

350

Captulo 12. Caracteres y cadenas de caracteres.

consecutivos a los de la cadena, hasta almacenar toda la informacin tecleada ms su carcter nulo final. Esa violacin de memoria puede tener y de hecho habitualmente tiene consecuencias desastrosas para el programa. Ejemplo: programa que pregunta el nombre y, entonces saluda al usuario. #include <stdio.h> int main(void) { char nombre[10]; printf("Cmo te llamas? "); gets(nombre); printf("Hola, %s.", nombre); return 0; } El programa est bien construido. Pero hay que tener en cuenta que el nombre que se introduce puede, fcilmente, superar los 10 caracteres. Por ejemplo, si un usuario responde diciendo Jos Antonio, ya ha introducido 13 caracteres: 4 por Jos, 7 por Antonio, 1 por el carcter en blanco, y otro ms por el carcter nulo final. En ese caso, el comportamiento del programa sera imprevisible. Se puede hacer un programa que acepte la entrada por teclado de carcter a carcter, y vaya mostrando la entrada por pantalla a la vez que la va almacenando en la cadena. Veamos una posible implementacin. #define TAM 30 #include <stdio.h> #include <ctype.h> int main(void) { char nombre[TAM]; short int i; printf("Cmo te llamas? "); for(i = 0 ; i < TAM ; i++) nombre[i] = NULL; i = 0; while(i < TAM) { nombre[i] = getchar();

351

Informtica Aplicada. Programacin en Lenguaje C.

if (nombre[i] == 10) { nombre[i] = NULL; break; } else if(nombre[i] == 8 && i > 0) { nombre[i] = NULL; printf("\b \b"); i--; } else if(isgraph(nombre[i]) || nombre[i] == 32) printf("%c",nombre[i++]); } printf("\n\nHola, %s.", nombre); return 0; } Donde el valor ASCII 8 es el carcter borrar uno hacia detrs: eso significa que hay que retroceder, escribir un blanco donde ya habamos escrito otro carcter, y volver a retroceder; adems hay que reducir en uno el valor del ndice i, puesto que hemos eliminado un carcter. Si estbamos en el carcter 0, la operacin de borrado no surte efecto, porque as lo hemos condicionado en el if correspondiente. El valor ASCII 10 es el carcter intro, que vamos a entender como final de entrada de la cadena: por eso se interrumpe el proceso de entrada de caracteres y se cambia el valor de ese ltimo carcter que deja de ser 10 para ser el carcter nulo. Y el carcter 32 es el carcter espacio en blanco, que no se resuelve como imprimible en la funcin isgraph. Esta pequea aplicacin, e incluso alguna un poco ms desarrollada, podran resolver el problema de la funcin gets, pero para nuestro estudio ser mejor usar la funcin de stdio.h sin complicarse la vida.

Operaciones con cadenas de caracteres.


Todo lo visto en el captulo de vectores es perfectamente aplicable a las cadenas: de hecho una cadena no es ms que un vector de tipo char. De todas formas, las cadenas de caracteres merecen un tratamiento

352

Captulo 12. Caracteres y cadenas de caracteres.

diferente al presentado para el resto de vectores, ya que las operaciones que se pueden realizar con cadenas son muy diferentes a las que se realizan con vectores numricos: concatenar cadenas, buscar una subcadena en una cadena dada, determinar cul de dos cadenas es mayor alfabticamente, etc. Vamos a ver algunos programas bsicos de manejo de cadena, y posteriormente presentaremos un conjunto de funciones de biblioteca definidas para las cadenas, y que se encuentran en la biblioteca string.h.

Copiar el contenido de una cadena en otra cadena: #include <stdio.h> int main(void) { char original[100]; char copia[100]; short int i = 0; printf(Cadena original ... ); gets(original); while(original[i] != NULL) { copia[i] = original[i]; i++; } copia[i] = NULL; printf("Original: %s\n",original); printf("Copa: %s\n",copia); return 0; } Mientras no se llega al carcter nulo de original, se van copiando uno a uno los valores de las variables de la cadena en copia. Al final, cuando ya se ha llegado al nulo en original, habr que cerrar tambin la cadena en copia, mediante un carcter nulo. El carcter nulo se escribe NULL, \0. Ambas formas son equivalentes.

353

Informtica Aplicada. Programacin en Lenguaje C.

Esta operacin tambin se puede hacer con una funcin de la biblioteca string: la funcin char *strcpy(char *dest, const char *src); que recibe como parmetros las dos cadenas, origen y destino, y devuelve la direccin de la cadena de destino. Con esta funcin, el programa antes presentado queda de la siguiente forma: #include <stdio.h> #include <string.h> int main(void) { char original[100], copia[100]; printf(Cadena original ... ); strcpy(copia, original); printf("Original: %s\n",original); printf("Copa: %s\n",copia); return 0; } gets(original);

Determinar la longitud de una cadena: #include <stdio.h> int main(void) { char original[100]; short int i = 0; printf(Cadena original ... ); gets(original); while(original[i] != NULL) i++; printf("%s tiene longitud %hd\n",original,i); return 0; } El contador i se va incrementando hasta llegar al carcter nulo. As, en i, tenemos la longitud de la cadena.

354

Captulo 12. Caracteres y cadenas de caracteres.

Esta operacin tambin se puede hacer con una funcin de la biblioteca string: la funcin size_t strlen(const char *s); que recibe como parmetro una cadena de caracteres y devuelve su longitud. Una variable tipo size_t es, para nosotros y ahora mismo, una variable de tipo entero. #include <stdio.h> #include <string.h> int main(void) { char original[100]; short int i; printf(Cadena original ... ); gets(original);

i = strlen(original); printf("%s tiene longitud %hd\n",original,i); return 0; }

Concatenar una cadena al final de otra: #include <stdio.h> int main(void) { char original[100]; char concat[100]; short int i = 0, j = 0; printf(Cadena original ... ); gets(original); printf(Cadena a concatenar ... ); gets(concat); while(original[i] != NULL) i++; while(concat[j] != NULL) { original[i] = concat[j]; i++; j++; } original[i] = NULL; printf("Texto concatenado: %s\n", original);

355

Informtica Aplicada. Programacin en Lenguaje C.

return 0; } Y de nuevo disponemos de una funcin en la biblioteca string que realiza la concatenacin de cadenas: char *strcat(char *dest, const char *src); Que recibe como parmetros las cadenas destino de la concatenacin y cadena fuente, y devuelve la direccin de la cadena que contiene la cadena original ms la concatenada. #include <stdio.h> #include <string.h> int main(void) { char original[100]; char concat[100]; printf(Cadena original ... ); gets(original); printf(Cadena a concatenar ... ); gets(concat); strcat(original, concat); printf("Texto concatenado: %s\n",original); return 0; } Tambin existe otra funcin, parecida a esta ltima, que concatena no toda la segunda cadena, sino hasta un mximo de caracteres, fijado por un tercer parmetro de la funcin: char *strncat(char *dest, const char *src, size_t maxlen);

Comparar dos cadenas e indicar cul de ellas es mayor, o si son iguales: #include <stdio.h> int main(void) { char cad01[100]; char cad02[100]; short int i = 0; short int chivato = 0; printf(Primera Cadena ... );

356

Captulo 12. Caracteres y cadenas de caracteres.

gets(cad01); printf(Segunda Cadena ... ); gets(cad02); while(cad01[i] != NULL && cad02[i] != NULL) { if(cad01[i] > cad02[i]) { chivato = 1; break; } else if(cad01[i] < cad02[i]) { chivato = 2; break; } i++; } if(chivato == 1) printf("cadena01 > cadena02"); else if(chivato == 2) printf("cadena02 > cadena01"); else if(cad01[i] == NULL && cad02[i] != NULL) printf("cadena02 > cadena01"); else if(cad01[i] != NULL && cad02[i] == NULL) printf("cadena01 > cadena02"); else printf("cadenas iguales"); return 0; } Y una vez ms, disponemos de una funcin en la biblioteca string que realiza la comparacin de cadenas: int strcmp(const char *s1, const char *s2); Que recibe como parmetros las cadenas a comparar y devuelve un valor negativo si s1 < s2; un valor positivo si s1 > s2; y un cero si ambas cadenas son iguales #include <string.h> #include <stdio.h> int main(void) { char c1[100] = "Texto de la cadena primera"; char c2[100] = "Texto de la cadena segunda"; short int comp;

357

Informtica Aplicada. Programacin en Lenguaje C.

printf(Primera Cadena ... ); gets(c1); printf(Segunda Cadena ... ); gets(c2); comp = strcmp(c1,c2); if(comp < 0) printf("cadena02 > cadena01"); else if(comp > 0) printf("cadena01 > cadena02"); else printf("Cadenas iguales"); return 0; } Tambin existe una funcin que compara hasta una cantidad de caracteres sealado, es decir, una porcin de la cadena: int strncmp(const char *s1, const char *s2, size_t maxlen); donde maxlen es el tercer parmetro, que indica hasta cuntos caracteres se han de comparar. Podramos seguir con otras muchas funciones de la biblioteca string. Hemos mostrado algunas de las operaciones con cadenas, con su funcin y sin ella, para presentar con ejemplos el modo habitual con el que se manejan las cadenas de caracteres. Hay mucha informacin sobre estas y otras funciones de la biblioteca string en cualquier ayuda on line de cualquier editor de C. Se recomienda consultar esa ayuda para obtener informacin sobre ellas.

Otras funciones de cadena.


Vamos a detenernos en la conversin de una cadena de caracteres, todos ellos numricos, en la cantidad numrica, para poder luego operar con ellos. Las funciones que veremos en este epgrafe se encuentran definidas en otras bibliotecas: en la stdlib.h o en la biblioteca math.h. Convertir una cadena de caracteres (todos ellos dgitos o signo decimal) en un double. double strtod(const char *s, char **endptr);

358

Captulo 12. Caracteres y cadenas de caracteres.

Esta funcin convierte la cadena s en un valor double. La cadena s debe ser una secuencia de caracteres que puedan ser interpretados como un valor double. La forma genrica en que se puede presentar esa cadena de caracteres es la siguiente: [ws] [sn] [ddd] [.] [ddd] [fmt[sn]ddd] donde [ws] es un espacio en blanco opcional; [sn] es el signo opcional (+ ); [ddd] son dgitos opcionales; [fmt] es el formato exponencial, tambin opcional, que se indica con las letras e E; finalmente, el [.] es el carcter punto decimal, tambin opcional. Por ejemplo, valores vlidos seran + 1231.1981 e-1; 502.85E2; + 2010.952. La funcin abandona el proceso de lectura y conversin en cuanto llega a un carcter que no puede ser interpretable como un nmero real. En ese caso, se puede hacer uso del segundo parmetro para detectar el error que ha encontrado: aqu se recomienda que para el segundo parmetro de esta funcin se indique el valor nulo: en esta parte del libro an no se tiene suficiente formacin en C para poder comprender y emplear bien este segundo parmetro. La funcin devuelve, si ha conseguido la transformacin, el nmero ya convertido en formato double. Vamos a ver un ejemplo. #include <stdio.h> #include <stdlib.h> int main(void) { char entrada[80]; double valor; printf("Nmero decimal ... "); valor = strtod(entrada, NULL); printf("La cadena es %s ", entrada); printf("y el nmero es %lf\n", valor); return 0; } gets(entrada);

359

Informtica Aplicada. Programacin en Lenguaje C.

De forma semejante se comporta la funcin atof, de la biblioteca math.h. Su prototipo es: double atof(const char *s); Donde el formato de la cadena de entrada es el mismo que hemos visto para la funcin strtod. Consultando la ayuda del compilador se puede ver cmo se emplean las funciones strtol y strtoul, de la biblioteca stdlib.h: la primera convierte una cadena de caracteres en un entero largo; la segunda es lo mismo pero el entero es siempre largo sin signo. Y tambin las funciones atoi y atol, que convierte la cadena de caracteres a int y a long int respectivamente. Como ejemplo de todo lo dicho, veamos un programa que acepta como entrada una cadena de la forma x + y = y muestra por pantalla el resultado de la operacin. Desde luego, el desarrollo presentado es uno de los muchos posibles; y como se ha dicho ya en otras ocasiones en este manual, no necesariamente la mejor de las soluciones: #include <stdio.h> #include <ctype.h> #include <stdlib.h> int main(void) { char e[80], n[20], op; short i = 0, j = 0; short est = 0, err = 0; long int a1, a2, res; printf("Introduzca la cadena de operacin."); printf("\nX + Y = o X - Y =... "); gets(e); while(e[i] != NULL) { if(e[i] != 32 && !isdigit(e[i]) && e[i] != '+' && e[i] != '-' && e[i] != '=') { err = 1; break; }

360

Captulo 12. Caracteres y cadenas de caracteres.

else if(e[i] == 32 && (est == 0 || est == 2 || est == 3 || est == 5)) i++; else if(isdigit(e[i]) && (est == 0 || est == 3)) { est++; n[j] = e[i]; i++; j++; } else if(isdigit(e[i]) && (est == 1 || est == 4)) { n[j] = e[i]; j++; i++; } else if(e[i] == 32 && (est == 1 || est == 4)) { n[j] = NULL; if(est == 1) a1 = atol(n); else a2 = atol(n); est++; j = 0; i++; } else if((e[i] == '+' || e[i] == '-') && est == 2) { op = e[i]; i++; est = 3; } else if(e[i] == '=' && est == 5) { printf("%lu %c %lu = ",a1,op,a2); printf("%lu", op=='+' ? a1 + a2 : a1 - a2); break; } else { err = 1; break; } } if(err == 1) printf("Error de entrada de datos"); return 0; }

361

Informtica Aplicada. Programacin en Lenguaje C.

Ejercicios.

12.1.

Escribir un programa que solicite del usuario la entrada de una cadena y muestre por pantalla en nmero de veces que se ha introducido cada una de las cinco vocales.

Mostramos una posible solucin: #include <stdio.h> int main(void) { char cadena[100]; short int a, e, i, o, u, cont; printf("Introduzca una cadena de texto ... \n"); gets(cadena); a = e = i = o = u = 0; for(cont = 0 ; cadena[cont] != NULL ; cont++) { if(cadena[cont] == 'a') a++; else if(cadena[cont] == 'e') e++; else if(cadena[cont] == 'i') i++; else if(cadena[cont] == 'o') o++; else if(cadena[cont] == 'u') u++; } printf("La cadena introducida ha sido ...\n"); printf("%s\n",cadena); printf("Y las vocales introducidas han sido ... \n"); printf("a ... %hd\n",a); printf("e ... %hd\n",e); printf("i ... %hd\n",i); printf("o ... %hd\n",o); printf("u ... %hd\n",u); return 0; } Una sola observacin al cdigo: la asignacin concatenada que pone a cero las cinco variables de cuenta de vocales. Esta sintaxis es correcta y est permitida en C.

362

Captulo 12. Caracteres y cadenas de caracteres.

12.2.

Escribir un programa que solicite del usuario la entrada de una cadena y muestre por pantalla esa misma cadena en maysculas.

Mostramos una posible solucin: #include <stdio.h> #include <ctype.h> int main(void) { char cadena[100]; short int cont; printf("Introduzca una cadena de texto ... \n"); gets(cadena); for(cont = 0 ; cadena[cont] != NULL ; cont++) cadena[cont] = toupper(cadena[cont]); printf("La cadena introducida ha sido ...\n"); printf("%s\n",cadena); return 0; }

12.3.

Escribir un programa que solicite del usuario la entrada de una cadena y elimine de ella todos los espacios en blanco. Imprima luego por pantalla esa cadena.

Este problema puede resolverse de dos maneras. La primera, sera haciendo la copia sin espacios en blanco en la misma cadena de origen: es decir, trabajando con una nica cadena de texto, que se modifica eliminando de ella todos los caracteres en blanco. La segunda es creando una segunda cadena y asignndole a sta los valores de los caracteres de la primera que no sean el carcter blanco. Una solucin de acuerdo con la primera forma sera la siguiente:

363

Informtica Aplicada. Programacin en Lenguaje C.

#include <stdio.h> #include <string.h> int main(void) { char cadena[100]; short int i, j; printf("Introduzca una cadena de texto ... \n"); gets(cadena); for(i = 0 ; cadena[i] != NULL ; ) if(cadena[i] == 32) for(j = i ; cadena[j] != NULL ; j++) cadena[j] = cadena[j + 1]; else i++; printf("La cadena introducida ha sido ...\n"); printf("%s\n",cadena); return 0; } Si el carcter i es el carcter blanco (ASCII 32) entonces no se incrementa el contador sino que se adelantan una posicin todos los caracteres hasta el final. Si el carcter no es el blanco, entonces simplemente se incrementa el contador y se sigue rastreando la cadena de texto. Una observacin: la estructura for situada inmediatamente despus de la funcin gets, controla una nica sentencia simple, que est controlada por una estructura ifelse, que a su vez controla otra sentencia for, tambin con una sola sentencia simple. No es necesario utilizar ninguna llave en todo ese cdigo porque no hay ni una sola sentencia compuesta. Una solucin de acuerdo con la segunda forma sera la siguiente: #include <stdio.h> #include <string.h> int main(void) { char original[100], copia[100]; short int i, j; printf("Introduzca una cadena de texto ... \n");

364

Captulo 12. Caracteres y cadenas de caracteres.

gets(original); for(i = 0 , j = 0 ; original[i] != NULL ; i++) if(original[i] != 32) copia[j++] = original[i]; copia[j] = 0; printf("La cadena introducida ha sido ...\n"); printf("%s\n",original); printf("La cadena transformada es ...\n"); printf("%s\n",copia); return 0; } Esta segunda forma es, desde luego, ms sencilla. Una observacin conviene hacer aqu: en la segunda solucin, al acabar de hacer la copia, hemos cerrado la cadena copia aadindole el carcter nulo de fin de cadena. Esa operacin tambin se debe haber hecho en la primera versin de resolucin del ejercicio, pero ha quedado implcita en el segundo anidado for. Intente comprenderlo.

12.4.

Cdigo de Julio Csar. Escribir un programa que solicite del usuario la entrada de una cadena y la entrada de un desplazamiento. Luego debe volver a imprimir la cadena con todos los caracteres alfabticos desplazados tantas letras en el alfabeto como indique el desplazamiento. Si en ese desplazamiento se llega ms all de la letra Z, entonces se contina de nuevo con la A. Se tienen 26 letras en el alfabeto ASCII.

#include <ctype.h> #include <stdio.h> int main(void) { char c[100]; short int i; short int d; printf("Introduzca una cadena de texto ... \n");

365

Informtica Aplicada. Programacin en Lenguaje C.

gets(c); for(i = 0 ; c[i] != NULL ; i++) c[i] = toupper(c[i]); printf("Introduzca el desplazamiento ... \n"); scanf( %hd",&d); d %= 26; /* Si d = 26 * k + d', donde d' < 26 entonces desplazar d en el abecedario es lo mismo que desplazar d'. */ for(i = 0 ; c[i] != NULL ; i++) { if(isalpha(c[i])) { c[i] += d; if(c[i] > 'Z') c[i] = 'A' + c[i] - 'Z' - 1; } } printf("La cadena transformada queda ... \n"); printf("%s",c); return 0; } Una nica observacin a este cdigo y, en general, a todos los que se presentan como ejemplo en este manual. Si bien vienen resueltos, cuando de verdad se aprende a programar es en el momento en que uno se pone delante de la pantalla y comienza a echar lneas de cdigo en busca de SU solucin. Este programa, como la mayora, tiene infinidad de formas diferentes de solventarlo. La que aqu se muestra no es la mejor, simplemente es la que se ha ocurrido a quien esto escribe en el tiempo que ha tardado en redactar esa solucin. Si hubiera resuelto el programa maana, el libro tendra una solucin distinta. Y es que copiar el cdigo en un editor de C y compilarlo no sirve para aprender. Que este manual es de C, y no de mecanografa.

12.5.

Escriba un programa que solicite al usuario una cadena de texto y genere luego otra a partir de la primera introducida donde nicamente se recojan los caracteres que sean letras: ni

366

Captulo 12. Caracteres y cadenas de caracteres.

dgitos, ni espacios, ni cualquier otro carcter. El programa debe, finalmente, mostrar ambas cadenas por pantalla. Sugerencia: usar la funcin isalpha de la biblioteca ctype.h, que devuelve un valor distinto de cero si recibe como parmetro el ASCII de un carcter alfabtico.

#include <stdio.h> #include <ctype.h> int main(void) { char cadena1[100], cadena2[100]; short i, j; printf("La primera cadena es ... "); for(i = 0, j = 0 ; i < 100 && cadena1[i] != '\0' ; i++) if(isalpha(cadena1[i])) cadena2[j++] = cadena1[i]; cadena2[j] = '\0'; printf("Cadena primera: %s\n", cadena1); printf("Cadena segunda: %s\n", cadena2); return 0; } gets(cadena1);

12.6.

Muestre

la

salida

que,

por

pantalla,

ofrece

el

siguiente

programa en C.

#include <stdio.h> #include <ctype.h> int main(void) { char c1[100] = "18 - junio - 2008"; char c2[100]; short i = 0, j = 0;

367

Informtica Aplicada. Programacin en Lenguaje C.

while(i < 100 && c1[i] != '\0') { if(isalpha(c1[i])) c2[j++] = c1[i]; i++; } c2[j] = '\0'; printf("La cadena c2 es ... %s", c2); return 0; }

Resuelto el ejercicio anterior, ste resulta muy sencillo. La estructura while recorre toda la cadena c1, y hace copia de sus valores en c2 cuando stos son alfabticos. As pues, este programa deja la cadena c2 con los caracteres alfabticos de la cadena c1. La solucin a la pregunta es que c2 contiene la cadena Junio

12.7.

Escriba un programa que reciba una cadena de texto y busque los caracteres que sean de tipo dgito (0, 1, , 9) y los sume. Por ejemplo, si la cadena de entrada es Hoy es 12 de septiembre de 2008, el clculo que debe realizar es la suma 1 + 2 (correspondientes al texto 12) + 2 + 0 + 0 + 8 (correspondientes al texto 2008). Observacin: el carcter 0 no tiene el valor numrico 0, ni el carcter 1 el valor numrico 1, y as sucesivamente. Pero se calcula fcilmente el valor de cualquier carcter dgito de la siguiente forma: Del carcter dgito 1 llegamos al valor numrico 1 restando 1 0. Del carcter dgito 2 llegamos al valor numrico 2 restando 2 0. Y as sucesivamente: del carcter dgito 9 llegamos al valor numrico 9 restando 9 0.

368

Captulo 12. Caracteres y cadenas de caracteres.

Hecha la advertencia recogida en el segundo prrafo del enunciado, el programa que debemos hacer tiene una apariencia semejante al presentado en el ejercicio anterior, utilizando ahora la funcin isdigit en lugar de la isalpha. Y cada vez que encontremos un carcter dgito calcularemos su valor numrico e iremos acumulando su suma en una variable numrica. El cdigo deber tener una apariencia como la que se muestra a continuacin:

#include <stdio.h> #include <ctype.h> int main(void) { char cadena[100]; short i; long Suma = 0; printf("Introduzca el valor de la cadena ... "); gets(cadena); for(i = 0 ; i < 100 && cadena[i] != '\0' ; i++) if(isdigit(cadena[i])) Suma += cadena[i] - '0'; printf("El valor de la suma es ... %ld", Suma); return 0; }

12.8.

Escriba

la

salida

que,

por

pantalla,

ofrece

el

siguiente

programa:

#include <stdio.h> #include <string.h> #define LNG 100 int main(void) { char cad1[LNG] = "3 DE SEPTIEMBRE DE 2008";

369

Informtica Aplicada. Programacin en Lenguaje C.

char cad2[LNG]; short int i, l; i = l = strlen(cad1); while(i) cad2[i - 1] = cad1[l - i--]; cad2[l] = '\0'; printf("cad1: %s\n", cad1); printf("cad2: %s\n", cad2); return 0; } Este programa copia la cadena cad1 en la cadena cad2, pero colocando los caracteres en orden inverso. La salida del programa es la siguiente: cad1: 3 DE SEPTIEMBRE DE 2008 cad2: 8002 ED ERBMEITPES ED 3

12.9.

Escriba un programa que reciba del usuario y por teclado una cadena de texto de no ms de 100 caracteres y a partir de ella se genere una cadena nueva con todas las letras minsculas y en la que se haya eliminado cualquier carcter que no sea alfanumrico. Ayuda: En la biblioteca ctype.h dispone de una funcin isalnum que recibe como parmetro un carcter y devuelve un valor verdadero si ese carcter es alfanumrico; de lo contrario el valor devuelto es cero. Y tambin dispone de la funcin tolower que recibe como parmetro un carcter y devuelve el valor del carcter en minscula si la entrada ha sido efectivamente un carcter alfabtico; y devuelve el mismo carcter de la entrada en caso contrario.

Ejercicio muy parecido a algunos de los anteriormente planteados. Exige el uso de algunas funciones de ctype.h.

370

Captulo 12. Caracteres y cadenas de caracteres.

Un posible cdigo para este enunciado podra ser el siguiente: #include <stdio.h> #include <ctype.h> int main(void) { char in[100], cp[100]; short i, j; printf("Cadena de entrada ... "); gets(in); for(i = 0 , j = 0 ; i < 100 && in[i] != NULL ; i++) if(isalnum(in[i])) cp[j++] = tolower(in[i]); cp[j] = '\0'; printf("Cadena copia ... %s", cp); return 0; }

12.10.

Escriba un programa que solicite del usuario dos cadenas de texto de cinco elementos (la longitud mxima de las cadenas ser por tanto de 4 caracteres) formadas por slo caracteres de dgito (0, 1, 2, 3, 4, 5, 6, 7, 8, 9), y que muestre por pantalla la suma de esas dos entradas.

En la solucin que se propone, no se hace una verificacin previa sobre la correcta entrada realizada por el usuario. Se supone que el usuario ha introducido dos enteros de, como mucho, 4 dgitos. Si no es as, el programa funcionar, peo no ofrecer un resultado vlido Lo que hacemos es convertir la cadena de dgitos en su valor entero. Se explica el procedimiento con un ejemplo: supongamos que la entrada para la cadena c1 ha sido 579: es decir, c1[0] = 5, c1[1] = 7, c[2] = 9; c1[3] = carcter nulo de fin de cadena; c1[4]: no se le ha asignado ningn valor.

371

Informtica Aplicada. Programacin en Lenguaje C.

Lo que hace el programa que proponemos como solucin es recorrer el array desde el primer elemento y hasta llegar al carcter nulo. Inicializada a cero, toma la variable n1 y le asigna, para cada valor del ndice i que recorre el array, el valor que tena antes multiplicado por 10, al que le suma el nuevo valor del array en la posicin i. Cuando i = 0 y n1 vale inicialmente 0, se lee en valor c1[0] (valor 5). Entonces n1 pasa a valer n1 * 10 + 5, es decir, 5. Cuando i = 1 y n1 vale 5, se lee en valor c1[1] (valor 7). Entonces n1 pasa a valer n1 * 10 + 7, es decir, 57. Cuando i = 2 y n1 vale 57, se lee en valor c1[2] (valor 9). Entonces n1 pasa a valer n1 * 10 + 9, es decir, 579. Ahora leer el siguiente valor que ser el de fin de cadena, y terminar el proceso de encontrar el valor numrico del entero introducido como cadena de caracteres. A lo explicado hay que hacer una sencilla aclaracin: al leer los dgitos en c1 lo que obtenemos es el valor ASCII de esos caracteres, y no su valor numrico. Para obtener el valor numrico basta restar por el ASCII del carcter 0. As, al carcter 0 lo tomaremos como 0 0, es decir, 48 48 = 0 (48 es el cdigo ASCII del carcter 0). Y al leer el carcter 1 lo tomaremos como 1 0, es decir, 49 48 = 1. Y as sucesivamente. El cdigo podra quedar de la siguiente manera: #include <stdio.h> int main(void) { char c1[5], c2[5]; short n1, n2, i; printf("Primera cadena ... "); printf("Segunda cadena ... "); gets(c1); gets(c2);

for(i = 0 , n1 = 0 ; c1[i] && i < 5 ; i++) n1 = n1 * 10 + (c1[i] - '0'); for(i = 0 , n2 = 0 ; c2[i] && i < 5 ; i++)

372

Captulo 12. Caracteres y cadenas de caracteres.

n2 = n2 * 10 + (c2[i] - '0'); printf("La suma vale ... %hd", n1 + n2); return 0; }

12.11.

Declare tres cadenas de texto. Asigne a la primera un valor mediante entrada de teclado. Asigne a la segunda cadena el valor que se le ha asignado a la primera. Asigne a la tercer el valor concatenado de la primera y de la segunda cadena. Utilice funciones de la biblioteca string.h.

#include <stdio.h> #include <string.h> #define LLL 100 int main(void) { char a[LLL], b[LLL], c[2 * LLL]; // ENTRADA DE LA CADENA a ... printf("Cadena a ... "); gets(a); // COPIA DE LA CADENA a EN b. strcpy(b, a); // CONCATENACION DE a Y b EN c. strcpy(c, a); strcat(c, b); // MOSTRAR CADENAS a, b, printf("Cadena a : printf("Cadena b : printf("Cadena c : return 0; } Y c. %s.\n", a); %s.\n", b); %s.\n", c);

373

Informtica Aplicada. Programacin en Lenguaje C.

12.12.

Declare dos cadenas de texto y asgneles valor mediante entrada de teclado. Mediante una funcin de string.h determine si la primera cadena es menor, igual o mayor que la segunda cadena.

El concepto de menor y mayor no tiene aqu relacin con la longitud de la cadena, sino con el contenido y, en concreto, con el orden alfabtico. Diremos, por ejemplo, que la cadena C es mayor que la cadena AAAAA, y que la cadena ABCDE es menor que la cadena ABCDEF.

#include <stdio.h> #include <string.h> #define LLL 100 int main(void) { char a[LLL], b[LLL]; // ENTRADA DE LA CADENA a ... printf("Cadena a ... "); printf("Cadena b ... "); gets(a); gets(b);

// DECIR CUAL DE LAS DOS CADENAS ES MAYOR if(strcmp(a, b) < 0) printf("%s menor que %s.\n", a, b); else if(strcmp(a, b) > 0) printf("%s mayor que %s.\n", a, b); else printf("Cadenas iguales.\n"); return 0; }

374

CAPTULO 13
MBITO Y VIDA DE LAS VARIABLES.
Este breve captulo pretende completar algunos conceptos presentados en el captulo 7 sobre los tipos de datos y variables. Ahora que hemos avanzado en el conocimiento del lenguaje C y que ya sabemos desarrollar nuestros pequeos programas, estos nuevos conceptos pueden ser comprendidos con claridad. Tambin presentamos una breve descripcin de cmo se gestiona el almacenamiento de los datos dentro del ordenador.

mbito y Vida.
Entendemos por mbito de una variable el lugar, dentro de un programa, en el que esta variable tiene significado. Hasta el momento todas nuestras variables han tenido como mbito todo el programa, y quiz ahora no es sencillo hacerse una idea intuitiva de este concepto;

Informtica Aplicada. Programacin en Lenguaje C.

pero realmente, no todas las variables estn en activo a lo largo de todo el programa. Adems del mbito, existe otro concepto, que podramos llamar extensin o tiempo de vida, que define el intervalo de tiempo en el que el espacio de memoria reservado por una variable sigue en reserva; cuando la variable muere, ese espacio de memoria vuelve a estar disponible para otros usos que el ordenador requiera. Tambin este concepto quedar ms aclarado a medida que avancemos en este breve captulo.

El almacenamiento de las variables y la memoria.


Para comprender las diferentes formas en que se puede crear una variable, es conveniente describir previamente el modo en que se dispone la memoria de datos en el ordenador. Hay diferentes espacios donde se puede ubicar una variable declarada en un programa: 1. Registros. El registro es el elemento ms rpido de almacenamiento y acceso a la memoria. La memoria de registro est ubicada directamente dentro del procesador. Sera muy bueno que toda la memoria fuera de estas caractersticas, pero de hecho el nmero de registros en el procesador est muy limitado. El compilador decide qu variables coloca en estas posiciones clave, la privilegiadas. conveniencia El o programador no tiene baza en esa decisin. El lenguaje C permite sugerir, mediante algunas palabras inconveniencia de que una determinada variable se cree en este espacio privilegiado de memoria. 2. La Pila. La memoria de pila reside en la memoria RAM (Random Access Memory: memoria de acceso aleatorio) De la memoria RAM es de lo que se habla cuando se anuncian los megas o gigas que tiene la memoria de un ordenador.

376

Captulo 13. mbito y vida de las variables.

El procesador tiene acceso y control directo a la pila gracias al puntero de pila, que se desplaza hacia abajo cada vez que hay que reservar ms memoria para una nueva variable, y vuelve a recuperar su posicin hacia arriba para liberar esa memoria. El acceso a la memoria RAM es muy rpido, slo superado por el acceso a registros. El compilador debe conocer, mientras est creando el programa, el tamao exacto y la vida de todas y cada una de las variables implicadas en el proceso que se va a ejecutar y que deben ser almacenados en la pila: el compilador debe generar el cdigo necesario para mover el puntero de la pila hacia abajo y hacia arriba. Esto limita el uso de esta buena memoria tan rpida. Hasta el captulo 10, cuando hablemos de la asignacin dinmica de la memoria, todas las variables que empleamos pueden existir en la pila, e incluso algunas de ellas en las posiciones de registro. 3. El montculo. Es un espacio de memoria, ubicada tambin en la memoria RAM, donde se crean las variables de asignacin dinmica. Su ventaja es que el compilador no necesita, al generar el programa, conocer cunto espacio de almacenamiento necesita asignar al montculo para la correcta ejecucin del cdigo compilado. Esta propiedad ofrece una gran flexibilidad al cdigo de nuestros programas. A cambio hay que pagar un precio con la velocidad: lleva ms tiempo asignar espacio en el montculo que tiempo lleva hacerlo en la pila. 4. Almacenamiento esttico. El almacenamiento esttico contiene datos que estn disponibles durante todo el tiempo que se ejecuta el programa. Ms adelante, en este captulo, veremos cmo se crean y qu caractersticas tienen las variables estticas. 5. Almacenamiento constante. Cuando se define un valor constante, ste se ubica habitualmente en los espacios de memoria reservados para el cdigo del programa: lugar seguro, donde no se ha de poder cambiar el valor de esa constante.

377

Informtica Aplicada. Programacin en Lenguaje C.

Variables Locales y Variables Globales.


Una variable puede definirse fuera de la funcin principal: en el programa, pero no en una funcin. Esas variables se llaman globales, y son vlidas en todo el cdigo que se escriba en ese programa. Su espacio de memoria queda reservado mientras el programa est en ejecucin. Diremos que son variables globales, que su mbito es todo el programa y que su vida perdura mientras el programa est en ejecucin. Veamos como ejemplo el siguiente cdigo: long int Fact; #include <stdio.h> int main(void) { short int n; printf("Introduce el valor de n ... "); scanf( %hd",&n); printf("El factorial de %hd es ... ",n); Fact = 1; while(n) Fact *=n--; printf("%ld",Fact); return 0; } La variable n es local: su mbito es nicamente el de la funcin principal main. La variable Fact es global: su mbito se extiende a todo el programa. Advertencia: salvo para la declaracin de variables globales (y

declaracin de funciones, que veremos ms adelante), el lenguaje C no admite ninguna otra sentencia fuera de una funcin. Ahora mismo este concepto nos queda fuera de intuicin porque no hemos visto an la posibilidad de crear y definir en un programa otras funciones, aparte de la funcin principal. Pero esa posibilidad existe, y

378

Captulo 13. mbito y vida de las variables.

en ese caso, si una variable es definida fuera de cualquier funcin, entonces esa variable es accesible desde todas las funciones del programa. No se requiere ninguna palabra clave del lenguaje C para indicar al compilador que esa variable concreta es global. Es suficiente con ubicar la declaracin fuera del mbito de cualquier bloque de cdigo. Se recomienda, en la medida de lo posible, no hacer uso de variables globales. Cuando una variable es manipulable desde cualquier mbito de un programa es fcil sufrir efectos imprevistos. Una variable ser local cuando se crea en un bloque del programa, que puede ser una funcin, o un bloque interno de una funcin. Por ejemplo: long x = 12; // Slo x est disponible. { long y = 25; // Tanto x como y estn disponibles. } // La variable y est fuera de mbito. Ha terminado su vida. El mbito de una variable local ser el del bloque en el que est definida. En C, puede declararse una variable local, con un nombre idntico al de una variable global; entonces, cuando en ese mbito local se haga referencia al nombre de esa variable, se entender la variable local: en ese mbito no se podr tener acceso a la variable global, cuyo nombre ha sido robado por una local. Por ejemplo: long x = 12; // Slo x est disponible. { long x = 25; // En este bloque la nica variable x accesible vale 25. } // La nica variable x en este mbito vale 12. Tambin pueden definirse variables locales del mismo nombre en mbitos diferentes y disjuntos, porque al no coincidir en mbito en

379

Informtica Aplicada. Programacin en Lenguaje C.

ninguna sentencia, no puede haber equvoco y cada variable, del mismo nombre, existe slo en su propio mbito. Por ejemplo: long x = 12; // Slo x est disponible. { long y = 25; // Tanto x como y estn disponibles. } // La variable y est fuera de mbito. Ha terminado su vida. { long y = 40; // Tanto x como y estn disponibles. // Esta variable y no es la misma que la otra. /* La declaracin de la variable y es correcta, puesto que la anterior declaracin de una variable con el mismo nombre fue en otro mbito. */ } // La variable y est fuera de mbito. Ha terminado su vida. Veamos un ejemplo sencillo de uso de diferentes variable locales: unsigned short i; for(i = 2 ; i < 10000 ; i++) { unsigned short suma = 1; unsigned short j; for(j = 2 ; j <= i / 2 ; j++) if(i % j == 0) suma += j; if(suma == i)printf(%hu,i) } En este cdigo, que como vimos permite buscar los nmeros perfectos entre los primeros 10000 enteros, declara dos variables (j y suma) en el bloque de la estructura del primer for. Al terminar la ejecucin del for gobernado por la variable i, esas dos variables dejan de existir. Si a su vez, la estructura for ms externa estuviera integrada dentro de otra estructura de iteracin, cada vez que se volviera a ejecutar ese for se volveran a crear esas dos variables, que tendran el mismo nombre, pero no necesariamente las mismas direcciones de memoria que antes. Hay una palabra en C para indicar que la variable es local. Es la palabra reservada auto. Esta palabra rara vez se utiliza, porque el compilador descubre siempre el mbito de las variables gracias al lugar donde se recoge la declaracin.

380

Captulo 13. mbito y vida de las variables.

Un ejemplo muy simple puede ayudar a presentar estas ideas de forma ms clara: #include <stdio.h> long b = 0, c = 0; int main(void) { for(b = 0 ; b < 10 ; b++) { long c = 0; c++; } printf("El valor de b es %ld y el de c es %ld", b, c); return 0; } Las variables b y c han sido declaradas globales. Y ambas han sido inicializadas a cero. Luego, dentro de la funcin principal, se ha declarado, local dentro del for, la variable c. Y dentro del for se han variado los valores de las variables b y c. Una aclaracin previa: cuando en un bloque controlado por una estructura de iteracin se declara una nueva variable, esa sentencia de declaracin slo se ejecuta en la primera iteracin. No ocurre que cada vez que se vuelve a iterar se vuelva a ejecutar la sentencia de declaracin. Y si en esa sentencia de declaracin de variable se asigna a dicha variable un valor inicial, tampoco se ejecuta la asignacin en cada nueva iteracin: repetimos que la sentencia declarativa slo se ejecuta la primera vez en cada llegada al bloque iterado, no en cada iteracin del bloque. Cul es la salida que ofrecer por pantalla este cdigo? Por lo que respecta a la variable b no hay ninguna duda: se ha incrementado diez veces, y su valor, despus de ejecutar la estructura for, ser 10. Pero, y c? Esta variable ha sufrido tambin una variacin y ha llegado al valor 10. Pero cul de los dos variables c ha cambiado?: la de mbito ms local. Y como la sentencia que ejecuta la funcin printf ya est fuera de la estructura for, y para entonces la variable local c ya ha muerto, la variable c que muestra la funcin printf no es otra que la

381

Informtica Aplicada. Programacin en Lenguaje C.

global: la nica viva en este momento. La salida que mostrar el programa es la siguiente: El valor de b es 10 y el de c es 0. Una advertencia importante: ya se ha visto que se pueden declarar, en mbitos ms reducidos, variables con el mismo nombre que otras que ya existen en mbitos ms globales. Lo que no se puede hacer es declarar, en un mismo mbito, dos variables con el mismo nombre. Ante esa circunstancia, el compilador dar error y no compilar. Una ltima observacin sobre las variables locales: El lenguaje C requiere que todas las variables se definan al principio del bloque donde tienen su mbito: esas declaraciones de variables deben ser las primeras sentencias en cada bloque que tenga variables locales en l. As, cuando el compilador crea el bloque, puede asignar el espacio exacto requerido para esas variables en la pila de la memoria. En C++ es posible diseminar las declaraciones de las distintas variables a lo largo del bloque, definindolas en el momento en que el programador requiere de su uso. Si se programa en un entorno de C++ (posiblemente ser su caso), se podr por tanto diseminar esas declaraciones; pero eso es propiedad de C++, y el programa generado no podra ser compilado en un compilador de C. Es conveniente por tanto, cuando se pretende aprender a programar en C, imponerse la disciplina de agrupar todas las declaraciones al principio de cada bloque, como es exigido en la sintaxis de C. Aunque el programa compilase en un compilador de C++, el cdigo sera sintcticamente errneo desde el punto de vista de un compilador de C.

Variables estticas y dinmicas.


Con respecto a la extensin o tiempo de vida, las variables pueden ser estticas o dinmicas. Ser esttica aquella variable que una vez definida, persiste hasta el final de la ejecucin del programa. Y ser

382

Captulo 13. mbito y vida de las variables.

dinmica aquella variable que puede ser creada y destruida durante la ejecucin del programa. No se requiere ninguna palabra clave para indicar al compilador que una variable creada es dinmica: toda variable lo es por defecto. S es en cambio necesario indicar al compilador, mediante la palabra clave static, cundo queremos que una variable sea creada esttica. Esa variable puede ser local, y en tal caso su mbito ser local, y slo podr ser usada cuando se estn ejecutando sentencias de su mbito; pero su extensin ser la misma que la del programa, y siempre que se vuelvan a las sentencias de su mbito all estar la variable, ya creada, lista para ser usada. Cuando terminen de ejecutarse las sentencias de su mbito esas posiciones de memoria no sern accesibles, porque estaremos fuera de mbito, pero tampoco podr hacerse uso de esa memoria para otras variables, porque la variable esttica seguir viva y esa posicin de memoria sigue almacenando el valor que qued de la ltima vez que se ejecutaron las sentencias de su mbito. Cuando se crea una variable local dentro de una bloque, o dentro de una funcin, el compilador reserva espacio para esa variable cada vez que se llama a la funcin: mueve en cada ocasin hacia abajo el puntero de pila tanto como sea preciso para volver a crear esa variable. Si existe un valor inicial para la variable, la inicializacin se realiza cada vez que se pasa por ese punto de la secuencia. Si se quiere que el valor permanezca durante la ejecucin del programa entero, y no slo cada vez que se entra de nuevo en el mbito de esa variable, entonces tenemos dos posibilidades: La primera consiste en crear esa variable como global, extendiendo su mbito al mbito de todo el programa (en este caso la variable no queda bajo control del bloque donde queramos ubicarla, o bajo control nico de la funcin que la necesita, sino que es accesible (se puede leer y se puede variar su valor) desde cualquier sentencia del programa); La segunda consiste en crear una variable static dentro del bloque o funcin. El

383

Informtica Aplicada. Programacin en Lenguaje C.

almacenamiento de esa variable no se lleva a cabo en la pila sino en el rea de datos estticos del programa. La variable slo se inicializa una vez la primera vez que se llama a la funcin, y retiene su valor entre diferentes invocaciones. Veamos el siguiente ejemplo, donde tenemos dos variables locales que sufren las mismas operaciones: una esttica (la variable que se ha llamado a) y la otra no (la que se ha llamado b): #include <stdio.h> int main(void) { long i, j, k; for(i = 0 ; i < 3 ; i++) for(j = 0 ; j < 4 ; j++) { static long a = 0; long b = 0; for(k = 0 ; k < 5 ; k++, a++, b++); printf("a = %3ld. b = %3ld.\n", a, b); } return 0; } El programa ofrece la siguiente salida por pantalla: a a a a a a a a a a a a = = = = = = = = = = = = 5. 10. 15. 20. 25. 30. 35. 40. 45. 50. 55. 60. b b b b b b b b b b b b = = = = = = = = = = = = 5. 5. 5. 5. 5. 5. 5. 5. 5. 5. 5. 5.

Primero observe que la tercera estructura for (la gobernada por la variable k), itera el punto y copa: no itera la funcin printf, que slo se ejecutar cuando se haya salido de esa estructura iterada.

384

Captulo 13. mbito y vida de las variables.

Esa tercera estructura iterada incrementa en 5 el valor de las variables a y b. Pero al ver la salida por pantalla comprobamos que a variable a mantiene guardado su valor para cada nueva entrad en el bloque

Variables en registro.
Cuando se declara una variable, se reserva un espacio de memoria para almacenar sus sucesivos valores. Cul sea ese espacio de memoria es cuestin que no podemos gobernar del todo. Especialmente, como ya se ha dicho, no podemos decidir cules son las variables que deben ubicarse en los espacios de registro. Pero el compilador, al traducir el cdigo, puede detectar algunas variables empleadas de forma recurrente, y decidir darle esa ubicacin preferente. En ese caso, no es necesario traerla y llevarla de la ALU a la memoria y de la memoria a la ALU cada vez que hay que operar con ella. El programador puede tomar parte en esa decisin, e indicar al compilador que alguna o algunas variables conviene que se ubiquen en los registros de la ALU. Eso se indica mediante la palabra clave register. Si al declarar una variable, se precede a toda la declaracin la palabra register, entonces esa variable queda creada en un registro de la ALU. Una variable candidata a ser declarada register es, por ejemplo, las que actan de contadoras en estructuras for. Tambin puede ocurrir que no se desee que una variable sea almacenada en un registro de la ALU. Y quiz se desea indicar al compilador que, sea cual sea su opinin, una determinada variable no debe ser almacenada all sino en la memoria, como una variable cualquiera normal. Para evitar que el compilador decida otra cosa se le indica con la palabra volatile.

385

Informtica Aplicada. Programacin en Lenguaje C.

El compilador tomas las indicaciones de register a ttulo orientativo. Si, por ejemplo, se ha asignado el carcter de register a ms variables que permite la capacidad de la ALU, entonces el compilador resuelve el conflicto segn su criterio, sin abortar el proceso de compilacin.

Variables extern.
Aunque estamos todava lejos de necesitar este tipo de declaracin, presentamos ahora esta palabra clave de C, que hace referencia al mbito de las variables. El lenguaje C permite trocear un problema en diferentes mdulos que, unidos, forman una aplicacin. Estos mdulos muchas veces sern programas independientes que despus se compilan por separado y finalmente se linkan o se juntan. Debe existir la forma de indicar, a cada uno de esos programas desarrollados por separado, la existencia de variables globales comunes para todos ellos. Variables cuyo mbito trasciende el mbito del programa donde se declaran, porque abarcan todos los programas que luego, linkados, darn lugar a la aplicacin final. Se podran declarar todas las variables en todos los archivos. C en la compilacin de cada programa por separado no dara error, y asignara tanta memoria como veces estuvieran declaradas. Pero en el enlazado dara error de duplicidad. Para evitar ese problema, las variable globales que deben permanecer en todos o varios de los mdulos de un programa se declaran como extern en todos esos mdulos excepto en uno, donde se declara como variable global sin la palabra extern. Al compilar entonces esos mdulos, no se crear la variable donde est puesta la palabra extern, y permitir la compilacin al considerar que, en alguno de los mdulos de linkado, esa variable s se crea. Evidentemente, si la palabra extern

386

Captulo 13. mbito y vida de las variables.

se coloca en todos los mdulos, entonces en ninguno se crea la variable y se producir error en el linkado. El identificador de una variable declarada como extern es conveniente que no tenga ms de seis caracteres, pues en los procesos de linkado de mdulos slo los seis primeros caracteres sern significativos.

En resumen
mbito: El mbito es el lugar del cdigo donde las sentencias pueden hacer uso de una variable. Una variable local queda declarada en el interior de un bloque. Puede indicarse ese carcter de local al compilador mediante la palabra auto. De todas formas, la ubicacin de la declaracin ofrece suficientes pistas al compilador para saber de la localidad de cada variable. Su mbito queda localizado nicamente a las instrucciones que quedan dentro del bloque donde ha sido creada la variable. Una variable es global cuando queda declarada fuera de cualquier bloque del programa. Su mbito es todo el programa: cualquier sentencia de cualquier funcin del programa puede hacer uso de esa variable global. Extensin: La extensin es el tiempo en que una variable est viva, es decir, en que esa variable sigue existiendo en la memoria. Una variable global debe existir mientras el programa est en marcha, puesto que cualquier sentencia del programa puede hacer uso de ella. Una variable local slo existe en el intervalo de tiempo transcurrido desde la ejecucin de la primera sentencia del bloque donde se ha creado esa variable y hasta que se sale de ese bloque. Es, tras la ejecucin de la ltima sentencia del bloque, el momento en que esa variable desaparece. Si el bloque vuelve a ejecutarse entonces vuelve a crearse una variable con su mismo nombre, que se ubicar donde antes,

387

Informtica Aplicada. Programacin en Lenguaje C.

o en otra direccin de memoria diferente: es, en todo caso, una variable diferente a la anterior. Se puede forzar a que una variable local exista durante toda la ejecucin del programa. Eso puede hacerse mediante la palabra reservada de C static. En ese caso, al terminar la ejecucin de la ltima instruccin del bloque donde est creada, la variable no desaparece. De todas formas, mientras no se vuelva a las sentencias de ese bloque, esa variable no podr ser reutilizada, porque fuera de ese bloque, an estando viva, est fuera de su mbito. Ubicacin: Podemos indicar al compilador si queremos que una variable sea creada en los registros de la ALU, utilizando la palabra reservada register. Podemos indicarla tambin al compilador que una variable no se cree en esos registros, mediante la palabra reservada volatile. Fuera de esas indicaciones que da el programador, el compilador puede decidir qu variables se crean en la ALU y cules en la memoria principal. No se ha dicho nada en este captulo sobre la creacin de espacios de memoria con valores constantes. Ya se present la forma de hacerlo en el captulo 2 sobre tipos de datos y variables en C. Una variable declarada como const quedar almacenada en el espacio de memoria de las instrucciones. No se puede modificar (mediante el operador asignacin) el valor de una variable definida como const. Por eso, al crear una variable de esta forma hay que asignarle valor en su declaracin.

Ejercicios.

13.1.

Haga un programa que calcule el mximo comn divisor de dos enteros que el usuario introduzca por consola. El usuario podr hacer tantos clculos como quiera, e interrumpir la bsqueda

388

Captulo 13. mbito y vida de las variables.

de nuevos mximos comunes divisores cuando introduzca un par de ceros. El programa mostrar por pantalla, cada vez que ejecute el cdigo del bucle, un valor contador que se incrementa y que indica cuntas veces se est ejecutando a lo largo de toda la aplicacin. Esa variable contador ser declarada como static.

#include <stdio.h> int main(void) { unsigned short a, b, mcd; do { printf("Valor de a ... "); scanf( %hu",&a); printf("Valor de b ... "); scanf( %hu",&b); if(a == 0 && b == 0) break; while(b) { static unsigned short cont = 0; mcd = b; b = a % b; a = mcd; cont++; printf("\ncont = %hu", cont); } printf("\n\nEl mcd es %hu.", mcd); }while(1); return 0; } Cada vez que se ejecuta el bloque de la estructura dowhile se incrementa en uno la variable cont. Esta variable se inicializa a cero nicamente la primera vez que se ejecuta la sentencia while de clculo del mximo comn divisor.

389

Informtica Aplicada. Programacin en Lenguaje C.

Observacin: quiz podra ser interesante, que al terminar de ejecutar todos los clculos que desee el usuario, entonces se mostrara por pantalla el nmero de veces que se ha entrado en el bucle. Pero eso no es posible tal y como est el cdigo, puesto que fuera del mbito de la estructura while que controla el clculo del mximo comn divisor, la variable cont, sigue viva, pero estamos fuera de mbito y el compilador no reconoce ese identificador como variable existente.

390

CAPTULO 14
FUNCIONES.
Hemos llegado a las funciones. Al principio del tema de estructuras de control sealbamos que haba dos maneras de romper el flujo secuencial de sentencias. Y hablbamos de dos tipos de instrucciones que rompen el flujo: las instrucciones condicionales y de las incondicionales. Las primeras nos dieron pie a hablar largamente de las estructuras de control: condicionales y de iteracin. Ahora toca hablar de las instrucciones incondicionales que realizan la transferencia a una nueva direccin del programa sin evaluar condicin alguna. Eso lo hacen las llamadas a las funciones. De hecho, ya hemos visto muchas funciones. Y las hemos utilizado. Cuando hemos querido mostrar por pantalla un mensaje hemos acudido a la funcin printf de la biblioteca stdio.h. Cuando hemos querido saber la longitud de una cadena hemos utilizado la funcin strlen, de la biblioteca string.h. Y cuando hemos querido hallar la funcin seno de un valor concreto, hemos acudido a la funcin sin, de math.h.

Informtica Aplicada. Programacin en Lenguaje C.

Y ya hemos visto que, sin saber cmo, hemos echado mano de una funcin estndar programada por ANSI C que nos ha resuelto nuestro problema. Cmo se ha logrado que se vea en pantalla un texto, o el valor de una variable? Qu desarrollo de Taylor se ha aplicado para llegar a calcular el seno de un ngulo dado? No lo sabemos. Dnde est el cdigo que resuelve nuestro problema? Tampoco lo sabemos. Pero cada vez que hemos invocado a una de esas funciones, lo que ha ocurrido es que el contador de programa ha abandonado nuestra secuencia de sentencias y se ha puesto con otras sentencias, que son las que codifican las funciones que hemos invocado. De forma incondicional, cada vez que se invoca una funcin, se transfiere el control de ejecucin a otra direccin de memoria, donde se encuentran codificadas otras sentencias, que resuelven el problema para el que se ha definido, editado y compilado esa funcin. Son transferencias de control con retorno. Porque cuando termina la ejecucin de la ltima de las sentencias de la funcin, entonces regresa el control a la sentencia inmediatamente posterior a aquella que invoc esa funcin. Quiz ahora, cuando vamos a abordar la teora de creacin, diseo e implementacin de funciones, ser buen momento para releer lo que decamos en captulos 5 y 6 al tratar de la modularidad. Y recordar tambin las tres propiedades que deban tener los distintos mdulos: independencia funcional, comprensibilidad, adaptabilidad. No lo vamos a repetir ahora: all se trat.

Definiciones.
Abstraccin Modularidad Programacin estructurada. Esas eran las tres notas bsicas que presentamos al presentar el lenguaje de programacin C. De la programacin estructurada ya hemos hablado, y lo seguiremos haciendo en este captulo. La abstraccin es el

392

Captulo 14. Funciones.

paso previo de toda programacin: conocer el sistema e identificar los ms significativos elementos que dan con la esencia del problema a resolver. Y la modularidad es la capacidad de dividir el sistema estudiado en partes diferenciadas. Eso que hemos llamado mdulo, en un lenguaje de programacin se puede llamar procedimiento o se puede llamar funcin. Las funciones y los procedimientos permiten crear programas complejos, mediante un reparto de tareas que permite construir el programa de forma estructurada y modular. Desde un punto de vista acadmico, se entiende por procedimiento el conjunto de sentencias a las que se asocia un identificador (un nombre), y que realiza una tarea que se conoce por los cambios que ejerce sobre el conjunto de variables. Y entendemos por funcin el conjunto de sentencias a las que se asocia un identificador (un nombre) y que genera un valor nuevo, calculado a partir de los argumentos que recibe. Los elementos que componen un procedimiento o funcin son, pues: 1. Un identificador, que es el nombre que sirve para invocar a esa funcin o a ese procedimiento. 2. Una lista de parmetros, que es el conjunto de variables que se facilitan al procedimiento o funcin para que realice su tarea modularizada. Al hacer la abstraccin del sistema, y modularlo en partes ms accesibles, hay que especificar los parmetros formales que permiten la comunicacin y definen el dominio (tipo de dato) de los datos de entrada. Esa lista de parmetros define el modo en que podrn comunicarse el programa que utiliza a la funcin y la funcin usada. 3. Un cuerpo o conjunto de sentencias. Las necesarias para poder realizar la tarea para la que ha sido definida la funcin o el procedimiento.

393

Informtica Aplicada. Programacin en Lenguaje C.

4. Un entorno. Entendemos por entorno el conjunto de variables globales, y externas por tanto al procedimiento o funcin, que pueden ser usadas y modificadas dentro del mbito de la funcin. Esas variables, por ser globales y por tanto definidas en un mbito ms amplio al mbito local de la funcin, no necesitan ser explicitadas en la lista de parmetros de la funcin. Es una prctica desaconsejable trabajar con el entorno de la funcin desde el mbito local de la funcin. Hacerlo lleva consigo que esa funcin deja de ser independiente de ese entorno y, por tanto, deja de ser exportable. Perderamos entonces el valor de la independencia funcional, que es una de las propiedades de la programacin por mdulos. Podemos pues concluir que el uso de variables globales dentro del cuerpo de un procedimiento o funcin es altamente desaconsejable. En el lenguaje C no se habla habitualmente de procedimientos, sino slo de funciones. Pero de hecho existen de las dos cosas. Procedimientos seran, por ejemplo, la funcin printf no se invoca para calcular valores nuevos, sino para realizar una tarea sobre las variables. Ms claro se ve con la funcin scanf que, efectivamente, realiza una tarea que se conoce por los cambios que ejerce sobre una variable concreta. Y funciones seran, por ejemplo, la funcin strlen, que a partir de una cadena de caracteres que recibe como parmetro de entrada calcula un valor, que es la longitud de esa cadena; o la funcin sin, que a partir de un ngulo que recibe como valor de entrada, calcula el seno de ese ngulo como valor de salida. En definitiva, una funcin es una porcin de cdigo, identificada con un nombre concreto (su identificador), que realiza una tarea concreta, que puede ser entendida de forma independiente al resto del programa, y que tiene muy bien determinado cmo se hace uso de ella, con qu parmetros se la invoca y bajo qu condiciones puede ser usada, cul es la tarea que lleva a cabo, y cul es el valor que calcula y devuelve.

394

Captulo 14. Funciones.

Tanto los procedimientos como las funciones pueden ser vistos como cajas negras: un cdigo del que desconocemos sus sentencias, al que se le puede suministrar unos datos de entrada y obtener modificaciones para esos valores de entrada y/o el clculo de un nuevo valor, deducido a partir de los valores que ha recibido como entrada. Con eso se consigue programas ms cortos; que el cdigo pueda ser usado ms de una vez; mayor facilidad para gestionar un correcto orden de ejecucin de sentencias; que las variables tengan mayor carcter local, y no puedan ser manipuladas fuera del mbito para el que han sido creadas.

Funciones en C.
Una funcin, en C, es un segmento independiente de cdigo fuente, diseado para realizar una tarea especfica. Esta tarea, o bien es el clculo de un resultado que se recibe en el mbito donde se invoc la funcin llamada, o bien realiza alguna operacin de salida de informacin por pantalla, o en algn archivo de salida, o en la red. Las funciones son los elementos principales de un programa en C. Cada una de las funciones de un programa constituye una unidad, capaz de realizar una tarea determinada. Quiz se podra decir que un programa es simplemente un conjunto de definiciones de distintas funciones, empleadas luego de forma estructurada. La primera funcin que aparece en todo programa C es la funcin principal, o funcin main. Todo programa ejecutable tiene una, y slo una, funcin main. Un programa sin funcin principal no genera un ejecutable. Y si todas las funciones se crean para poder ser utilizadas, la funcin principal es la nica que no puede ser usada por nadie: nadie puede invocar a la funcin principal de un programa. Tampoco puede llamarse a s misma (este concepto de autollamada, denominado recurrencia, lo trataremos ms adelante en el siguiente captulo).

395

Informtica Aplicada. Programacin en Lenguaje C.

Adems de la funcin principal, en un programa se pueden encontrar otras funciones: o funciones creadas y diseadas por el programador para esa aplicacin, o funciones ya creadas e implementadas y compiladas en libreras: de creacin propia o adquirida o pertenecientes al estndar de ANSI C. Las funciones estndar de ANSI C se encuentran clasificadas en distintas libreras de acuerdo con las tareas que desarrollan. Al montar un programa en C, se buscan en las libreras las funciones que se van a necesitar, que se incluyen en el programa y se hacen as parte del mismo. Tambin se pueden crear las propias funciones en C. As, una vez creadas y definidas, ya pueden ser invocadas tantas veces como se quiera. Y as, podemos ir creando nuestras propias bibliotecas de funciones. Siempre que hemos hablado de funciones hemos utilizado dos verbos, uno despus del otro: creacin y definicin de la funcin. Y es que en una funcin hay que distinguir entre su declaracin o prototipo (creacin de la funcin), su definicin (el cuerpo de cdigo que recoge las sentencias que debe ejecutar la funcin para lograr llevar a cabo su tarea) y, finalmente, su invocacin o llamada: una funcin creada y definida slo se ejecuta si otra funcin la invoca o llama. Y en definitiva, como la nica funcin que se ejecuta sin ser invocada (y tambin la nica funcin que no permite ser invocada) es la funcin main, cualquier funcin ser ejecutada nicamente si es invocada por la funcin main o por alguna funcin que ha sido invocada por la funcin main o tiene en su origen, en una cadena de invocacin, una llamada desde la funcin main.

396

Captulo 14. Funciones.

Declaracin de la funcin.
La declaracin de una funcin se realiza a travs de su prototipo. Un prototipo tiene la forma: tipo_funcion nombre_funcion([tipo1 [var1] [, tipoN [varN]]); Donde tipo_funcion declara de qu tipo es el valor que devolver la funcin. Una funcin puede devolver valores de cualquier tipo de dato vlido en C, tanto primitivo como diseado por el programador (se ver la forma de crear tipos de datos en unos temas ms adelante). Si no devuelve ningn valor, entonces se indica que es de tipo void. Donde tipo1,, tipoN declara de qu tipo es cada uno de los valores que la funcin recibir como parmetros al ser invocada. En la declaracin del prototipo es opcional indicar el nombre que tomarn las variables que recibirn esos valores y que se comportarn como variables locales de la funcin. Sea como sea, ese nombre s deber quedar recogido en la definicin de la funcin. Pero eso es adelantar acontecimientos. Al final de la declaracin viene el punto y coma. Y es que la declaracin de una funcin es una sentencia en C. Una sentencia que se consigna fuera de cualquier funcin. La declaracin de una funcin tiene carcter global dentro de programa donde se declara. No se puede declarar, ni definir, una funcin dentro de otra funcin: eso siempre dar error de compilacin. Toda funcin que quiera ser definida e invocada debe haber sido previamente declarada. El prototipo de la funcin presenta el modo en que esa funcin debe ser empleada. Es como la definicin de su interface, de su forma de comunicacin: qu valores, de qu tipo y en qu orden debe recibir la funcin como argumentos al ser invocada. El prototipo permite localizar cualquier conversin ilegal de tipos entre los argumentos utilizados en la llamada de la funcin y los tipos definidos en los parmetros, entre los parntesis del prototipo. Adems, controla

397

Informtica Aplicada. Programacin en Lenguaje C.

que el nmero de argumentos usados en una llamada a una funcin coincida con el nmero de parmetros de la definicin. Existe una excepcin a esa regla: cuando una funcin es de tipo int, puede omitirse su declaracin. Pero es recomendable no hacer uso de esa excepcin. Si en una expresin, en una sentencia dentro del cuerpo de una funcin, aparece un nombre o identificador que no ha sido declarado previamente, y ese nombre va seguido de un parntesis de apertura, el compilador supone que ese identificador corresponde al nombre de una funcin de tipo int. Todas las declaraciones de funcin deben preceder a la definicin del cuerpo de la funcin main.

Definicin de la funcin.
Ya tenemos la funcin declarada. Con el prototipo ha quedado definido el modo en que podemos utilizarla: cmo nos comunicamos nosotros con ella y qu resultado nos ofrece. Ahora queda la tarea de definirla. Hay que escribir el cdigo, las sentencias, que van a realizar la tarea para la que ha sido creada la funcin. La forma habitual que tendr la definicin de una funcin la conocemos ya, pues hemos visto ya muchas: cada vez que hacamos un programa, y escribamos la funcin principal, estbamos definiendo esa funcin main. Esa forma es: tipo_funcion nombre_funcion([tipo1 var1][, tipoN varN]) { [declaracin de variables locales] [cuerpo de la funcin: grupo de sentencias] [return(parmetro);] } Donde el tipo_funcion debe coincidir con el de la declaracin, lo mismo que nombre_funcion y lo mismo que la lista de parmetros. Ahora, en la

398

Captulo 14. Funciones.

definicin, los parmetros de la funcin siguen recogiendo el tipo de dato y el nombre de la variable: pero ahora ese nombre NO es opcional. Debe ponerse, porque esos nombres sern los identificadores de las variables que recojan los valores que se le pasan a la funcin cuando se la llama o invoca. A esas variables se las llama parmetros formales: son variables locales a la funcin: se crean cuando la funcin es invocada y se destruyen cuando se termina la ejecucin de la funcin. La lista de parmetros puede ser una lista vaca porque no se le quiera pasar ningn valor a la funcin: eso es frecuente. En ese caso, tanto en el prototipo como en la definicin, entre los parntesis que siguen al nombre de la funcin se coloca la palabra clave void. tipo_funcion nombre_funcion(void); // declaracin del prototipo Si la funcin no devuelve valor alguno, entonces se indica como de tipo void, al igual que ya se hizo en la definicin del prototipo. Una funcin declarada como de tipo void no puede ser usada como operando en una expresin de C, porque esa funcin no tiene valor alguno. Una funcin de tipo void puede mostrar datos por pantalla, escribir o leer ficheros, etc. El bloque de la funcin tiene tres partes: la declaracin de las variables locales, el cuerpo de la funcin, donde estn las sentencias que llevarn a cabo la tarea para la que ha sido creada y definida la funcin, y la sentencia return, de la que hablaremos enseguida y que ya hemos utilizado innumerables veces en la funcin main. El bloque de la funcin viene recogido entre llaves. Aunque la funcin tenga una sola sentencia, es obligatorio recoger esa sentencia nica entre las llaves de apertura y de cerrado. Las variables creadas en el cuerpo de la funcin sern locales a ella. Se pueden usar identificadores idnticos para nombrar distintas variables de diferentes funciones, porque cada variable de cada funcin pertenece a un mbito completamente disjunto al mbito de otra funcin, y no hay

399

Informtica Aplicada. Programacin en Lenguaje C.

posibilidad alguna de confusin. Cada variable tendr su direccin y su mbito distintos. Aunque ya se ha dicho anteriormente, recordamos que todas las funciones en C, sin excepcin alguna, estn en el mismo nivel de mbito, es decir, no se puede declarar ninguna funcin dentro de otra funcin, y no se puede definir una funcin como bloque interno en el cuerpo de otra funcin.

Llamada a la funcin.
La llamada a una funcin es una sentencia habitual en C. Ya la hemos usado con frecuencia, invocando hasta el momento nicamente funciones de biblioteca. Pero la forma de invocar es la misma para cualquier funcin. nombre_funcion([argumento1][, , argumentoN]); La sentencia de llamada est formada por el nombre de la funcin y sus argumentos (los valores que se le pasan) que deben ir recogidos en el mismo orden que la secuencia de parmetros del prototipo y entre parntesis. Si la funcin no recibe parmetros (porque as est definida), entonces se coloca despus de su nombre los parntesis de apertura y cerrado sin ninguna informacin entre ellos. Si no se colocan los parntesis, se produce un error de compilacin. El paso de parmetros en la llamada exige una asignacin para cada parmetro. El valor del primer argumento introducido en la llamada a la funcin queda asignado en la variable del primer parmetro formal de la funcin; el segundo valor de argumento queda asignado en el segundo parmetro formal de la funcin; y as sucesivamente. Hay que asegurar que el tipo de dato de los parmetros formales es compatible en cada caso con el tipo de dato usado en lista de argumentos en la llamada de la funcin. El compilador de C no dar error si se fuerzan cambios de

400

Captulo 14. Funciones.

tipo

de

dato

incompatibles,

pero

el

resultado

ser

inesperado

totalmente. La lista de argumentos estar formada por nombres de variables que recogen los valores que se desean pasar, o por literales. No es necesario (ni es lo habitual) que los identificadores de los argumentos que se pasan a la funcin cuando es llamada coincidan con los identificadores de los parmetros formales. Las llamadas a las funciones, dentro de cualquier funcin, pueden realizarse en el orden que sea necesario, y tantas veces como se quiera, independientemente del orden en que hayan sido declaradas o definidas. Incluso se da el caso, bastante frecuente como veremos ms adelante, que una funcin pueda llamarse a s misma. Esa operacin de autollamada se llama recurrencia. Si la funcin debe devolver un valor, con cierta frecuencia interesar que la funcin que la invoca almacene ese valor en una variable local suya. En ese caso, la llamada a la funcin ser de la forma: variable = nombre_funcion([argumento1][, , argumentoN]); Aunque eso no siempre se hace necesario, y tambin con frecuencia encontraremos las llamadas a las funciones como partes de una expresin.

La sentencia return.
Hay dos formas ordinarias de terminar la ejecucin de una funcin. 1. Llegar a la ltima sentencia del cuerpo, antes de la llave que cierra el bloque de esa funcin. 2. Llegar a una sentencia return. La sentencia return fuerza la salida de la funcin, y devuelve el control del programa a la funcin que la llam, en la sentencia inmediatamente posterior a la de la llamada a la funcin.

401

Informtica Aplicada. Programacin en Lenguaje C.

Si la funcin es de un tipo de dato distinto de void, entonces en el bloque de la funcin debe recogerse, al menos, una sentencia return. En ese caso, adems, en esa sentencia y a continuacin de la palabra return, deber ir el valor que devuelve la funcin: o el identificador de una variable o un literal, siempre del mismo tipo que el tipo de la funcin o de otro tipo compatible. Una funcin tipo void no necesariamente tendr la sentencia return. En ese caso, la ejecucin de la funcin terminar con la sentencia ltima del bloque. Si una funcin de tipo void hace uso de sentencias return, entonces en ningn caso debe seguir a esa palabra valor alguno: si as fuera, el compilador detectar un error y no compilar el programa. La sentencia return puede encontrarse en cualquier momento del cdigo de una funcin. De todas formas, no tendra sentido recoger ninguna sentencia ms all de una sentencia return que no estuviera condicionada, pues esa sentencia jams llegara a ejecutarse. En resumen, la sentencia return realiza bsicamente dos operaciones: 1. Fuerza la salida inmediata del cuerpo de la funcin y se vuelve a la siguiente sentencia despus de la llamada. 2. Si la funcin no es tipo void, entonces adems de terminar la ejecucin de la funcin, devuelve un valor a la funcin que la llam. Si esa funcin llamante no recoge ese valor en una variable, el valor se pierde, con todas las variables locales de la funcin abandonada. La forma general de la sentencia return es: return [expresin]; Muchos programadores habitan a colocar la expresin del return entre parntesis. Es opcional, como lo es en la redaccin de cualquier expresin.

402

Captulo 14. Funciones.

Si el tipo de dato de la expresin del return no coincide con el tipo de la funcin entonces, de forma automtica, el tipo de dato de la expresin se convierte en el tipo de dato de la funcin. Ha llegado el momento de ver algunos ejemplos. Veamos primero una funcin de tipo void: una que muestre un mensaje por pantalla. Declaracin: void mostrar(short); Definicin: void mostrar(short x) { printf(El valor recibido es %hd., x); } Llamada: mostrar(10); que ofrece la siguiente salida por pantalla: El valor recibido es 10. Otro ejemplo: Una funcin que reciba un entero y devuelva el valor de su cuadrado. Declaracin: unsigned long int cuadrado(short); Definicin: unsigned long int cuadrado(short x) { return x * x; } Una posible llamada: printf(El cuadrado de %hd es %ld.\n, a, cuadrado(a)); Un tercer ejemplo, ahora con dos sentencias return: una funcin que reciba como parmetros formales dos valores enteros y devuelve el valor del mayor de los dos: Declaracin: short mayor(short, short); Definicin: short mayor(short x, short y)

403

Informtica Aplicada. Programacin en Lenguaje C.

{ if(x > y) return x; else return y; } Desde luego la palabra else podra omitirse, porque jams se llegar a ella si se ejecuta el primer return, y si la condicin del if es falsa, entonces se ejecuta el segundo return. Otra posible definicin: short mayor(short x, short y) { x > y ? return(x) : return(y); } O tambin: short mayor(short x, short y) { return x > y ? x : y; } Llamada: A = mayor(a,b); Donde la variable A guardar el mayor de los dos valores entre a y b. Una ltima observacin: el tipo de la funcin puede ser un tipo de dato puntero. En ese caso el valor que devuelve la funcin ser una direccin de memoria donde se aloja un valor, o un vector, o una matriz. Ms adelante, en los siguientes captulos, veremos algn ejemplo donde este tipo de funcin resulta de gran utilidad.

mbito y vida de las variables.


Ya conocemos el concepto de mbito de la variable. Y ahora que ya sabemos algo de las funciones, es conveniente presentar cundo se puede acceder a cada variable, cundo diremos que est viva, etc. Veamos un programa ya conocido, el del clculo del factorial de un entero, resuelto ahora mediante funciones:

404

Captulo 14. Funciones.

#include <stdio.h> long Factorial(short); int main(void) { short n; printf("Introduzca el valor de n ... "); scanf( %hd", &n); printf("El factorial de %hd ",n); printf("es %ld",Factorial(n)); return 0; } long Factorial(short a) { long F = 1; while(a) F *= a--; return F; } En este programa, la funcin principal main tiene definida una variable de tipo short, a la que hemos llamado n. En esa funcin, esa variable es local, y podemos recoger sus caractersticas en la cudrupla: <n, short, , >

La variable, de tipo short, n se almacena en la direccin de memoria Rn y guardar el valor que reciba de la funcin scanf. La funcin main invoca a la funcin Factorial. En la llamada se pasa como parmetro el valor de la variable n. En esa llamada, el valor de la variable n se copia en la variable a de Factorial: <a, short, , >

Desde el momento en que se produce la llamada a la funcin Factorial, abandonamos el mbito de la funcin main. En este momento, la variable n est fuera de mbito y no puede, por tanto hacerse uso de ella. No ha quedado eliminada: estamos en el mbito de Factorial pero an no han terminado todas las sentencias de main. En el clculo dentro de la funcin Factorial se ve modificado el valor de la variable local a.

405

Informtica Aplicada. Programacin en Lenguaje C.

Pero esa modificacin para nada influye en la variable n, que est definida en otra posicin de memoria distinta. Cuando se termina la ejecucin de la funcin Factorial, el control del programa vuelve a la funcin main. La variable a y la variable F mueren, pero el valor de la variable F ha sido recibido como parmetro en la funcin printf, y as podemos mostrarlo por pantalla. Ahora, de nuevo en la funcin principal, volvemos al mbito de la variable n, de la que podramos haber hecho uso si hubiera sido necesario. Veamos ahora otro ejemplo, con un programa que calcule el mximo comn divisor de dos enteros. De nuevo, resolvemos el problema mediante funciones: #include <stdio.h> long euclides(long, long); int main(void) { long n1, n2; do { printf("Introduzca el valor de n1 ... "); scanf( %ld", &n1); printf("Introduzca el valor de n2 ... "); scanf( %ld", &n2); if(n2 != 0) { printf("\nEl mcd de %ld y %ld ,n1, n2); printf(es %ld\n", euclides(n1,n2)); } }while(n2 != 0); return 0; } long euclides(long a, long b) { static short cont = 0; long mcd; while(b) { mcd = b; b = a % b;

406

Captulo 14. Funciones.

a = mcd; } printf("Invocaciones a la funcin ... %hd\n", ++cont); return mcd; } En esta ocasin, adems, hemos incluido una variable static en la funcin euclides. Esta variable nos informar de cuntas veces se ha ejecutado la funcin. Las variables n1 y n2, de main, dejan de estar accesibles cuando se invoca a la funcin euclides. En ese momento se copian sus valores en las variables a y b que comienzan a existir precisamente en el momento de la invocacin de la funcin. Adems de esas variables locales, y de la variable local mcd, se ha creado otra, llamada cont, que es tambin local a euclides pero que, a diferencia de las dems variables locales, no desaparecer cuando se ejecute la sentencia return y se devuelva el control de la aplicacin a la funcin main: es una variable declarada static. Cuando eso ocurra, perder la variable cont su mbito, y no podr ser accedida, pero en cuanto se invoque de nuevo a la funcin euclides, all estar la variable, ya creada, accesible para cuando la funcin la requiera.

Recapitulacin.
Hemos visto como crear bloques de cdigo que pueden luego ser invocados, mediante un nombre para que realicen una determinada funcin. Gracias a la llamada a uno de estos bloques de cdigo, que hemos llamado funciones, podemos obtener un resultado calculado a partir de valores iniciales que el bloque de cdigo o funcin recibe como parmetros. La funcin termina su ejecucin entregando un valor concreto de un tipo de dato predeterminado y establecido en su prototipo.

407

Informtica Aplicada. Programacin en Lenguaje C.

Ejercicios.

14.1.

Escribir un programa que solicite al usuario dos enteros y calcule, mediante una funcin, el mximo comn divisor. Definir otra funcin que calcule el mnimo comn mltiplo, teniendo en cuenta que siempre se verifica que

a b = mcd(a, b) mcm(a, b) .

#include <stdio.h> // Declaracin de las funciones ... short mcd(short, short); long mcm(short, short); // Funcin principal... int main(void) { short a, b; printf("Introduzca el valor de a ... "); scanf( %hd",&a); printf("Introduzca el valor de b ... "); scanf( %hd",&b); printf("El mximo comn divisor de %hd y %hd ", a, b); printf("es %hd", mcd(a,b)); printf("\ny el mnimo comn mltiplo %ld.", mcm(a,b)); return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ /* Funcin clculo del mximo comn divisor. ------------- */ short mcd(short a, short b) { short m; while(b) { m = a % b; a = b;

408

Captulo 14. Funciones.

b = m; } return a; } /* Funcin clculo del mnimo comn mltiplo. ------------ */ long mcm(short a, short b) { return a * (long)b / mcd(a, b); }

14.2.

Haga un programa que calcule el trmino n (a determinar en la ejecucin del programa) de la serie de Fibonacci. El programa deber utilizar una funcin, llamada fibonacci, cuyo prototipo sea short fibonacci(short); Que recibe como parmetro el valor de n, y devuelve el trmino n-simo de la Serie.

#include <stdio.h> #include <conio.h> unsigned long fibonacci(short); int main(void) { short N; printf("Indique el trmino de la serie: "); scanf( %hd", &N); printf("\nEl trmino %hd es %lu.", N, fibonacci(N)); return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ /* Funcin Fibonacci. ------------------------------------ */

409

Informtica Aplicada. Programacin en Lenguaje C.

unsigned long fibonacci(short x) { unsigned long fib1 = 1 , fib2 = 1, Fib = 1; while(x > 2) { Fib = fib1 + fib2; fib1 = fib2; fib2 = Fib; x--; } return Fib; }

14.3.

Escriba un programa que solicite al usuario un entero y devuelva el factorial de ese entero. Utilice una funcin para el clculo del factorial.

#include <stdio.h> // Declaracin de la funcin Factorial ... unsigned long factorial(short); // Funcin principal ... int main(void) { short N; printf("Indique un entero menor que 13: "); scanf( %hd", &N); printf("\nEl factorial de %hd, N); printf( es %lu.", factorial(N)); return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ /* Funcin Factorial. ------------------------------------ */ unsigned long factorial(short x) { unsigned long F = 1;

410

Captulo 14. Funciones.

while(x) F *= x--; return F; }

14.4.

Escriba una funcin que reciba un entero y diga si es o no es perfecto (devuelve 1 si lo es; 0 si no lo es). Utilice esa funcin para mostrar los nmeros perfectos entre dos enteros introducidos por el usuario.

#include <stdio.h> #include <conio.h> // Declaracin de la funcin perfecto ... short perfecto(long); // Funcin principal ... int main(void) { long a, b; printf("Limite inferior ... "); scanf( %ld",&a); printf("Limite superior ... "); scanf( %ld",&b); for(long i = a ; i <= b; i++) if(perfecto(i)) printf("%6ld", i); return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ // Funcin perfecto ... short perfecto(long x) { long suma = 1; for(long div = 2 ; div <= x / 2 ; div++) if(x % div == 0) suma += div; return suma == x; }

411

Informtica Aplicada. Programacin en Lenguaje C.

14.5.

El

binomio

de

Newton

proporciona

la

expansin

de

las

potencias de una suma:

donde

Escriba un programa que solicite al usuario los valores de , , y muestre entonces por pantalla el valor usando la expansin del binomio de Newton. Sugerencias: (1) Emplear la funcin double pow(double, double); (primer parmetro: base; segundo parmetro: exponente) recogida en math.h. (2) Ser ms cmodo definir una funcin que realice el clculo del factorial.

El ejercicio es bastante sencillo, y el cdigo ya ha quedado visto anteriormente. #include <stdio.h> #include <math.h> long factorial(short); long factorial(short n) { long F = 1; while(n) F *= n--; return F; } int main(void) { short n, k; double x, y, sumando, binomio = 0; printf("Valor de n ... "); scanf( %hd", &n); printf("Valor de x ... "); scanf( %lf", &x); printf("Valor de y ... "); scanf( %lf", &y);

412

Captulo 14. Funciones.

for(k = 0 ; k <= n ; k++) { sumando = factorial(n); sumando /= ((double)factorial(k)*factorial(n-k)); sumando *= pow(x, n - k) * pow(y, k); binomio += sumando; } printf("Resultado = %lf.", binomio); return 0; }

14.6.

Se llama PRIMO PERFECTO a aquel entero primo n que verifica que ((n - 1)) 2 tambin es primo. Por ejemplo, n = 11 es primo y ((n - 1)) 2 = 5 tambin es primo: luego n = 11 es un primo perfecto. Haga un programa que muestre los primos perfectos menores que 1000. Sugerencia: se simplifica mucho el cdigo si define una funcin, llamada, por ejemplo, int esprimo(long), que devuelve un entero distinto de cero cuando el entero que se recibe como parmetro es un valor primo.

Ya hemos visto en anteriores captulos cmo determinar si un entero es primo o compuesto. Es tarea que ya sabemos hacer. El enunciado es sencillo: decir, en un intervalo de enteros, cules son primos perfectos: es decir: no basta que el nmero se primo, sino que adems debe serlo su mitad menos uno. Y eso tambin sabemos cmo se hace, porque vuelve a ser lo mismo que antes. Aqu la dificultad puede estar en que el cdigo se puede hacer un poco farragoso. Por eso, disponer de una funcin a la que pasarle un entero y que devuelva un valor distinto de cero si el entero es primo adecentara mucho el cdigo.

413

Informtica Aplicada. Programacin en Lenguaje C.

Una posible solucin podra ser la siguiente: #include <stdio.h> #include <math.h> int esprimo(long); int main(void) { long N; for(N = 5 ; N < 1000 ; N += 2) if(esprimo(N) && esprimo((N - 1) / 2)) printf("%ld\n", N); return 0; } int esprimo(long x) { short d; if(x % 2 == 0) return 0; for(d = 3 ; d <= sqrt(x) ; d += 2) if(x % d == 0) return 0; return 1; } Efectivamente, la definicin de la funcin principal logra una gran simplicidad, gracias a la declaracin de la segunda funcin.

14.7.

Suponga que ha definido una funcin cuyo prototipo es long isPrime(long); Dicha funcin devuelve el mismo entero que recibi como parmetro si ste era primo, y devuelve el menor de sus divisores propios, si la entrada corresponda a un nmero compuesto. Utilizando esta funcin, escriba un programa (funcin

principal) que muestre por pantalla todos los factores primos de un entero introducido por el usuario.

414

Captulo 14. Funciones.

El enunciado es parecido al anterior, porque de nuevo utilizamos una funcin que determina su el entero que recibe como parmetro es primo o compuesto. Pero ahora queremos que la funcin d algo ms de informacin: en caso de que el entero no sea primo, queremos que devuelva el entero ms pequeo que lo divide. As, como se ver, podremos obtener la factorizacin de un entero de una forma sencilla y con una funcin main fcil de implementar. Una posible implementacin de la funcin podra ser la siguiente: #include <math.h> long isPrime(long a) { long d; for(d = 2 ; d <= sqrt(a) ; d++) if(a % d == 0) return d; return a; } Y una posible implementacin de la funcin main que invoca a la funcin recin definida podra ser la siguiente: #include <stdio.h> int main(void) { long x; // Valor de entrada a factorizar. long d; // Recoge la salida de isPrime(). printf("Introduce x ... "); scanf( %ld", &x); printf("1\t"); // El 1 es el primer factor. while((d = isPrime(x)) != 1) { printf("%ld\t", d); //Sucesivos factores. x /= d; } return 0; }

415

Informtica Aplicada. Programacin en Lenguaje C.

14.8.

Escriba el cdigo de una funcin, cuyo prototipo es short digito(char); que recibe un carcter y devuelve el valor -1 si dicho carcter no es numrico; y en caso contrario devuelve su valor como nmero. Por ejemplo, si recibe el carcter 3 devuelve el valor 3; si recibe el carcter p devuelve el valor -1.

Utilizando la funcin isdigit de ctype.h, el problema se resuelve con bastante sencillez: #include <ctype.h> short digito(char a) { return isdigit(a) ? a - '0' : -1; } Si es no es dgito devuelve el valor 1. Si es dgito devuelve el valor numrico del carcter dgito recibido como parmetro. Si no se quiere usar la funcin isdigit, bastar algo tambin bastante sencillo, como por ejemplo: short digito(char a) { return a >= 0 && a <= 9 ? a - '0' : -1; } Si el carcter est entre los caracteres 0 y 0 (ambos inclusive), entonces la funcin devuelve el valor numrico de ese dgito, obtenido restando, al ASCII del carcter codificado en la variable a, el ASCII del carcter 0.

14.9.

El

clculo

de

la

raz

cuadrada

de

un

nmero

se

ha

desarrollado de diferentes maneras a lo largo de los siglos, segn los conocimientos matemticos de cada poca. Uno de esos mtodos es el conocido como mtodo Babilnico. Se

416

Captulo 14. Funciones.

basa en el hecho de que el lado de un cuadrado es la raz cuadrada de su rea. Este mtodo toma los dos lados ( imaginario de superficie ( y ) de un rectngulo

) y, de forma iterativa, y ; y en vale y

modifica los tamaos de esos lados hasta lograr que sean iguales o casi iguales. Inicialmente toma cuadrado, el nuevo vale cada iteracin destinada a convertir el rectngulo en un , y luego el nuevo . El proceso se repite hasta lograr que la diferencia entre sea menor que un valor lmite de aproximacin dado. Suponga la siguiente funcin principal, que recibe del usuario un valor y el valor del lmite a la aproximacin del clculo:

#include <stdio.h> int main(void) { double x, aprox; printf("Valor de x ... "); scanf( %lf", &x); printf("Aproximacion ... "); scanf( %lf", &aprox); printf("La RC de %lf es %lf", x, RC(x, aprox)); return 0; } Indique el prototipo de la funcin RC y escriba un posible cdigo para esta funcin.

El propotipo es el siguiente: double RC(double, double); Y un posible cdigo podra ser el siguiente: double RC(double x, double e) { double a = 1 , b = x; while((a > b && (a - b) > e) || (a < b && (b - a) > e)) {

417

Informtica Aplicada. Programacin en Lenguaje C.

a = (a + b) / 2; b = x / a; } return a; }

14.10.

Haga un programa que solicite al usuario un valor entero entre 1 y 999.999 y escriba por pantalla la cantidad numrica introducida escrito con letras. Por ejemplo, si el usuario introduce 56343, el programa deber escribir por pantalla Cincuenta y seis mil trescientos cuarenta y tres.

El programa planteado es bastante largo y el enunciado desanima, porque de entrada se vislumbran tantas posibilidades que aburre. Sin embargo su lgica es sencilla y gracias a las funciones es tambin fcil de entender. En la solucin que aqu se presenta, han quedado definidas tres funciones. Una llamada unidades, otra decenas, y otra centenas. Al obtener el nmero a leer en formato texto, lo primero que hace la funcin principal es determinar si, efectivamente, ese nmero introducido tiene unidades, decenas, centenas, unidades de millar, decenas de millar y centenas de millar. Segn las tenga o no se invocar a las funciones que expresan las unidades, o las decenas, o las centenas. La funcin de las decenas es algo ms complicada porque ha de contemplar la forma de expresar la numeracin entre el once y el quince, distinto al resto de decenas. Y adems ha de tener en cuenta que si no hay unidades, entonces las decenas no aaden, al final, la cpula y. #include <stdio.h> #include <string.h>

418

Captulo 14. Funciones.

// Declaracin de las funciones char* unidades(unsigned short, unsigned short); char* decenas(unsigned short, unsigned short); char* centenas(unsigned short,unsigned short,unsigned short); /Funcin principal int main(void) { char leido[200], leidoM[200]; unsigned long int n, naux; unsigned short C, D, U, c, d, u; do { // Inicializamos las cadenas de caracteres. leido[0] = '\0'; leidoM[0] = '\0'; printf("\n\n\nIntroduzca entero a ... "); scanf( %lu",&naux); // Determinamos unidades, decenas, centenas... // y unidades, decenas y centenas de mil n = naux; u = naux % 10; naux /= 10; d = naux % 10; naux /= 10; c = naux % 10; naux /= 10; U = naux % 10; naux /= 10; D = naux % 10; naux /= 10; C = naux % 10; if(U || D || C) { // Sin el nmero es mayor que 999: strcat(leidoM,centenas(C,D,U)); strcat(leidoM,decenas(U,D)); strcat(leidoM,unidades(U,D)); strcat(leidoM, "mil "); } strcat(leido,centenas(c,d,u)); strcat(leido,decenas(u,d)); strcat(leido,unidades(u,d)); strcat(leidoM,leido); printf("El numero %lu se lee ... ",n); printf("\n%s",leidoM); }while(n); // Termina la aplicacin cuando el usuario

419

Informtica Aplicada. Programacin en Lenguaje C.

// quiera leer el nmero cero. return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ /* Funcin para las unidades. ---------------------------- */ char* unidades(unsigned short u, unsigned short d) { if(d == 1 && (u == 0 || u == 1 || u == 2 || u == 3 || u == 4 || u == 5)) return ""; switch(u) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: case 8: case 9: } } /* Funcin para las decenas. ----------------------------- */ char* decenas(unsigned short u, unsigned short d) { if(d == 1 && (u == 0 || u == 1 || u == 2 || u == 3 || u == 4 || u == 5)) switch(u) { case 0: return "diez "; case 1: return "once "; case 2: return "doce "; case 3: return "trece "; case 4: return "catorce "; case 5: return "quince "; } switch(d) { case 0: return ""; case 1: return "dieci"; case 2: if(u) return "veinti"; else return "veinte"; case 3: if(u) return "treinta y ";

return return return return return return return return return return

""; "uno "; "dos "; "tres "; "cuatro "; "cinco "; "seis "; "siete "; "ocho "; "nueve ";

420

Captulo 14. Funciones.

case 4: case 5: case 6: case 7: case 8: case 9: } }

else return "treinta "; if(u) return "cuarenta y "; else return "cuarenta "; if(u) return "cincuenta y "; else return "cincuenta"; if(u) return "sesenta y "; else return "sesenta "; if(u) return "setenta y "; else return "setenta "; if(u) return "ochenta y "; else return "ochenta "; if(u) return "noventa y "; else return "noventa ";

/* Funcin para las centenas. ---------------------------- */ char* centenas(unsigned short c,unsigned short d, unsigned short u) { switch(c) { case 0: return ""; case 1: if(!u && !d) return "cien "; else return "ciento "; case 2: return "doscientos "; case 3: return "tresientos "; case 4: return "cuatrocientos "; case 5: return "qinientos "; case 6: return "seiscientos "; case 7: return "setecientos "; case 8: return "ochocientos "; case 9: return "novecientos "; } }

421

Informtica Aplicada. Programacin en Lenguaje C.

422

CAPTULO 15
RECURSIVIDAD (RECURRENCIA).
Para entender la recursividad primero hay que entender la recursividad. Tratamos ahora de un concepto de gran utilidad para la programacin: la recursividad. La recursividad es un comportamiento presente en la misma naturaleza de muchos problemas para los que buscamos soluciones informticas. En este captulo queremos presentar el concepto de recursividad o de recurrencia, y mostrar luego las etapas habituales para disear algoritmos recursivos. Presentamos tambin el concepto de induccin, que trae de la mano al de recurrencia y que permite en muchos casos llegar a una correcta definicin de un algoritmo recurrente o recursivo. Para la redaccin de este captulo se ha empleado la siguiente bibliografa: Diseo y verificacin de algoritmos. Programas recursivos. Francisco Perales Lpez

Informtica Aplicada. Programacin en Lenguaje C.

Edita: Universitat de les Illes Balears. Palma 1998. Collecci Materials Didctics, n. 56. Captulo 3: Diseo del algoritmos recursivos.

Resolviendo un ejercicio
Supongamos que deseamos hacer un programa sencillo que vaya solicitando del usuario valores enteros y vaya luego mostrando por pantalla esos valores (no le exigimos que realiza ninguna operacin ni proceso alguno con esos valores) hasta que el usuario introduzca un entero que sea primo. En ese caso, el programa mostrar ese ltimo entero y terminar su ejecucin. Ya conocemos cmo usar las funciones. Podemos definir una primer funcin que llamaremos esPrimo, que recibe como parmetro un entero unsigned long, y devuelve un entero corto sin signo que ser cero si el entero recibido como parmetro es compuesto, y cualquier valor distinto de cero si el entero recibido como parmetro es primo. Su prototipo ser: unsigned short esPrimo(unsigned long); Tambin podemos definir una segunda funcin, llamada esCompuesto, que se comporte como la anterior pero al revs: devuelve un valor verdadero si el entero recibido como parmetro es primo, y un valor distinto de cero si es compuesto. Su prototipo ser: unsigned short esCompuesto(unsigned long); Y la definicin de esta segunda funcin es muy sencilla, una vez tengamos definida la primera: unsigned short esCompuesto(unsigned long N) { return !esPrimo(N); }

424

Captulo 15. Recursividad (Recurrencia)

Simplemente devuelve, negado, el valor devuelto por la funcin esPrimo. La funcin principal de esta aplicacin que acabamos de enunciar puede tener la siguiente forma: int main(void) { unsigned long a; do { printf(Entrada entero ... ); scanf( %lu, &a); printf(Valor introducido ... %lu\n, a); }while(esCompuesto(a)); return 0; } Tenemos pendiente la implementacin de la funcin esPrimo. Ser sencilla, porque adems ya hemos trabajado este algoritmo (ver, por ejemplo, ejercicio 10.9) Un posible cdigo podra ser el siguiente: unsigned short esPrimo(unsigned long N) { unsigned short d; for(d = 2; d <= sqrt(N) && N % d ; d++); return (unsigned short)(N % d); } Devolver el resto del cociente entre N y el valor de la variable d con el que se haya terminado la serie de iteraciones del for. Si N es compuesto, entonces ese cociente ser cero; de lo contrario devolver el valor del resto, que ser distinto de cero. Esta funcin busca entre los valores consecutivos de d (entre 2 y la raz cuadrada de N) alguno que divida a N (cuyo resto sea falso, es decir, cero) Valor tras valor, simplemente incrementa d hasta salir del rango indicado o hasta encontrar uno que divida a N. Luego, ya fuera de la iteracin, devuelve el valor del resto: si la iteracin ha terminado porque se ha encontrado un valor para d que verifica que N % d es cero (falso), entonces la funcin esPrimo devolver el valor 0, que es el del resto del cociente entre esos dos nmeros; si la iteracin ha terminado porque no

425

Informtica Aplicada. Programacin en Lenguaje C.

se ha encontrado ningn entero que divida a N, y se ha llegado a un valor de d mayor que la raz cuadrada de N, entonces ese valor de d no dividir a N y el resto ser distinto de cero (verdadero). Podemos pretender mejorar el algoritmo de la funcin esPrimo. Lo haramos, quiz, si la iteracin no recorriera todos los valores de d entre 2 y la raz de N sino que tomara en consideracin nicamente a los valores primos del intervalo. El cdigo de la funcin esPrimo con esa nueva definicin podra ser el siguiente: unsigned short esPrimo(unsigned long N) { unsigned short d; for(d = 2; d <= sqrt(N) && N % d ; d = siguientePrimo(d)); return (unsigned short)(N % d); } Quedara ahora pendiente definir la funcin siguientePrimo. Esta funcin recibe como parmetro un valor unsigned short, y devuelve otro valor unsigned short. Su prototipo sera el siguiente: unsigned short siguientePrimo(unsigned short); Y una posible definicin podra ser la siguiente: unsigned short siguientePrimo(unsigned short N) { do N++; while(esCompuesto(N)); return N; } Simplemente incrementa el valor recibido de uno en uno hasta alcanzar uno que no sea compuesto. Si ahora analizamos con detalle todo el cdigo desarrollado hasta el momento en este captulo, tenemos que la funcin main invocar repetidamente a una funcin llamada esPrimo, que a su vez invoca repetidamente a una funcin llamada siguientePrimo, que a su vez invoca una vez a la funcin esCompuesto, que a su vez invoca a la funcin esPrimo.

426

Captulo 15. Recursividad (Recurrencia)

Pero llegados a este punto nos encontramos con que la funcin esPrimo desencadena una secuencia de llamadas a funciones en la que ella misma est implicada. Es eso vlido? Si se dan algunas condiciones, la respuesta es que s. Nuestro ejemplo ha incurrido en llamadas recursivas o recurrentes. De esto vamos a tratar en el presente captulo.

El concepto de recursividad.
La palabra recursividad no aparece en el diccionario de la Real Academia. Al menos en su 22 edicin del ao 2001 (que es la ltima edicin disponible, on line, en la web de la RAE, al menos en septiembre de 2010). Tampoco aparece en el Diccionario de Mara Moliner. La palabra castellana que se emplea para tratar de lo que se va a hablar en este captulo es la de Recurrencia. El diccionario de la RAE define as esta palabra: 1. f. Cualidad de recurrente. 2. f. Mat. Propiedad de aquellas secuencias en las que cualquier trmino se puede calcular conociendo los precedentes. De todas formas se ha mantenido el trmino recursividad en el ttulo del captulo porque esta palabra s se emplea de forma habitual en el lenguaje informtico y aparece en cualquier referencia o manual que verse sobre algoritmia. Aqu emplearemos ambas palabras de forma indistinta. La recursividad est presente en muchos sistemas del mundo real. Y por eso, porque muchos problemas que queremos afrontar tiene una esencia recursiva, la recursividad es una herramienta conveniente y muy til para disear modelos informticos de la realidad que deseamos modelizar. Se dice que un sistema es recursivo cuando est parcial o

completamente definido en trminos de s mismo.

427

Informtica Aplicada. Programacin en Lenguaje C.

Quiz todos nos hemos planteado alguna vez una imagen recursiva: una fotografa que muestra a una persona que sostiene entre sus manos esa fotografa, que muestra a esa persona que sostiene entre sus manos esa fotografa, que muestra a esa persona que sostiene entre sus manos esa fotografa, que muestra Una imagen recursiva podemos definirla, por ejemplo, de la siguiente manera. Se toma un cuadrado, se divide en nueve cuadrados y se elimina el cuadro central. Con cada uno de los ocho cuadrados restantes se realiza la misma operacin: dividir en nueve cuadrados y eliminar el central. Y con cada uno de ellos de nuevo lo mismo, y lo mismo con cada uno de los cuadrados que quedan, y as hasta el infinito. Por ejemplo, en la figura 15.1. se representa este dibujo de definicin recurrente. Este cuadro puede parecer intil. No se trata ahora de explicar sus muchas virtudes, sino de presentar el concepto de recursividad. El cuadro representa el conocido conjunto de Cantor en el plano. Basta ponerse en Google y buscar conjunto de Cantor para lograr encontrar dibujos y aplicaciones prcticas de este conjunto. Se pueden seguir buscando representaciones grficas de curvas cuya definicin es recurrente: la curva de Peano, la de Hilbert, la de Hock, o de los fractales. Pero ese es otro asunto.

Figura 15.1.: Conjunto de Cantor

428

Captulo 15. Recursividad (Recurrencia)

Pero adems de poder mostrar con este concepto algunas curiosidades aparentemente intiles (slo aparentemente), tambin podemos presentar otros ejemplos de recurrencia. Hay muchos problemas matemticos que se resuelven mediante tcnicas recursivas. Podemos acudir a muchos de los problemas planteados anteriores. El algoritmo de Euclides es recurrente. El mximo comn divisor de dos nmeros segundo: y ( ) se puede definir como el mximo comn ) y del resto de dividir el primero con el . La secuencia se debe parar cuando divisor del segundo (de en los captulos

se alcanza un valor de segundo entero igual a cero: de lo contrario en la siguiente vuelta se realizara una divisin por cero. l clculo del factorial: podemos definir el factorial de un entero positivo uno. La bsqueda de un trmino de la serie de Fibonacci: Podemos definir la serie de Fibonacci como aquella cuyo trmino los trminos y : el lmite cuando es igual a la suma de . Y de nuevo se tiene ( ) como el producto de con el factorial de : . Y de nuevo esta definicin tiene un lmite: el momento en el

que se llega al valor cero: el factorial de cero es, por definicin, igual a

donde el valor del trmino es la unidad.

Clculo de una fila del tringulo de Tartaglia (cfr. Captulo 6). Es conocida la propiedad del tringulo de Tartaglia segn la cual, si calculamos el valor de cualquier elemento situado en el primer o ltimo lugar de cada fila, el valor de ese elemento es igual a uno; y el valor de cualquier otro elemento del tringulo resulta igual a la suma de los elementos de su fila anterior que estn encima de la posicin que deseamos calcular. Es decir: ; ; .

429

Informtica Aplicada. Programacin en Lenguaje C.

donde

, y donde siempre se verifica que

Y as tenemos que para calcular una fila del tringulo de Tartaglia no es necesario acudir al clculo de binomios ni de factoriales, y basta con conocer la fila anterior, que se calcula si se conoce la fila anterior, que se calcula si se conoce la fila anterior hasta llegar a las dos primeras filas, que estn formadas todo por valores uno. Tenemos, por tanto, y de nuevo, un camino recurrente hecho a base de simples sumas para encontrar la solucin que antes habamos buscado mediante el clculo de tres factoriales, dos productos y un cociente para cada elemento. En todos los casos hemos visto un camino para definir un procedimiento recurrente que llega a la solucin deseada. En los captulos anteriores no hemos llegado a soluciones recurrentes, sino que nicamente hemos hecho uso de estructuras de control iterativas. Ahora, en todas estas soluciones, podemos destacar dos caractersticas bsicas de todo procedimiento recurrente o recursivo: 1. Lo indicaba el diccionario de la RAE: Propiedad de aquellas secuencias en las que cualquier trmino se puede calcular conociendo los precedentes. La recurrencia se puede usar en aquellos sistemas donde sepamos llegar a un valor a partir de algunos valores precedentes. Es la propiedad que implica recurrencia. 2. Es imprescindible que, adems, haya unos valores iniciales

preestablecidos hacia los que se converge: unos valores que no se definen por recurrencia. En el algoritmo de Euclides se para el proceso en cuanto se llega a dos valores tales que su mdulo es cero. En el algoritmo del clculo del factorial siempre se llega a un valor mnimo establecido en el cero: . En el algoritmo de Fibonacci tenemos que los dos primeros elementos de la sucesin son unos. Y para el tringulo de Tartaglia tenemos que todos los extremos de todas las filas son tambin iguales a uno.

430

Captulo 15. Recursividad (Recurrencia)

La recursividad es una herramienta de diseo de algoritmos aceptada en la mayora de los lenguajes de programacin. Esos lenguajes permiten la creacin de una funcin que hace referencia a s misma dentro de la propia definicin. Eso supone que al invocar a una funcin recursiva, sta genera a su vez una o varias nuevas llamadas a ella misma, cada una de las cuales genera a su vez una o varias llamadas a la misma funcin, y as sucesivamente. Si la definicin est bien hecha, y los parmetros de entrada son adecuados, la cadena de invocaciones termina felizmente en alguna o varias llamadas que no generan nuevas invocaciones. Esas llamadas finales terminan su ejecucin y devuelven el control a la llamada anterior, que finaliza su ejecucin y devuelve el control a la llamada anterior, y as retornando el camino de llamadas emprendido, se llega a la llamada inicial y se termina el proceso. Cuando un procedimiento recursivo se invoca por primera vez decimos que su profundidad de recursin es 1 y el procedimiento se ejecuta a nivel 1. Si la profundidad de recursin de un procedimiento es pasa al nivel y de nuevo se llama a s mismo, entonces desciende un nivel de recursin y . Cuando el procedimiento regresa a la instruccin de donde fue llamado asciende un nivel de recursin. Si una funcin contiene una referencia explcita a s mismo, entonces contiene una la cual tiene (directa o indirectamente) una

decimos que tenemos una recursividad directa. Si referencia a otra funcin referencia a indirecta.

, entonces decimos que tenemos una recursividad

Veamos el ejemplo de una funcin recursiva que calcula el factorial de un entero que recibe como parmetro de entrada: Funcin ( Entero) Entero. 1. Si Entonces Sino (es decir, ) Entonces 2. Fin. Supongamos que invocamos al procedimiento con el valor . La

lista de llamadas que se produce queda recogida en la Tabla 15.1. Hay

431

Informtica Aplicada. Programacin en Lenguaje C.

un total de seis llamadas al procedimiento definido para el clculo del factorial, hasta que se le invoca con el parmetro que hace es devolver directamente el valor 1. , que es el nico que en lugar de devolver un valor calculado con una nueva llamada, lo

Nivel de Recursin 1 2 3 4 5 6

5 4 3 2 1 0

devuelve 5 * Factorial(4) 4 * Factorial(3) 3 * Factorial(2) 2 * Factorial(1) 1 * Factorial(0) 1

recibe 24 6 2 1 1 -

resultado 120 24 6 2 1 1

Tabla 15.1.: Llamadas al procedimiento definido para el clculo del Factorial para el caso de N 5 .

El cdigo de este algoritmo toma la siguiente forma: #include <stdio.h> long Fact(short); int main(void) { short a; printf("Valor de a ... "); scanf( %hd", &a); printf("El factorial de %hd es %ld.\n", a, Fact(a)); return 0; } long Factorial(short A) { if(A == 0) return 1; else return a * Factorial(A - 1); } De una forma ms compacta, la funcin Fact podra definirse como: long Fact(short n) {

432

Captulo 15. Recursividad (Recurrencia)

return n ? (long)n * Fact(n - 1) : 1; }

La demostracin por induccin o recurrencia.


Vamos a ver a continuacin del principio de demostracin por recurrencia o induccin sobre los nmeros naturales. Es conveniente tratar de ello fuera del contexto informtico, porque esta forma de demostracin muestra un modo de razonamiento esencial para el correcto dominio de la programacin recursiva. La demostracin por recurrencia se emplea para establecer que una determinada propiedad es cierta para todo nmero natural. Existen distintas formas de plantear la demostracin por induccin. Aqu recogemos la de uso ms habitual. Se demuestra que es cierto para todo natural a partir de los siguientes

dos pasos de razonamiento: a) Base de la induccin o recurrencia: Demostrar que el entero 0. b) Recurrencia: se demuestra que la propiedad es hereditaria. Para ello se establece una hiptesis de induccin: se supone que cierto para el natural entonces es cierto para . es , y con base a eso se debe demostrar que es cierto para

Ejemplos de esta forma de demostracin hay muchos. Presentamos aqu uno: supongamos que queremos demostrar que

Podemos demostrarlo con los dos pasos antes indicados: a) Es cierto para :

433

Informtica Aplicada. Programacin en Lenguaje C.

b) Supongamos que es cierto para un determinado valor :

Debemos entonces, demostrar que se cumple para

cqd.

Definicin recurrente de un conjunto.


As como se pueden demostrar numerosas propiedades de los enteros mediante la induccin o recurrencia, tambin se pueden definir conjuntos. La nocin de conjunto juega un papel importante en la programacin y es, por tanto, esencial saber definir conjuntos. La induccin o recurrencia aporta un mtodo para definir muy interesante, pues permite establecer reglas de construccin de los conjuntos: permite por tanto dar una definicin de conjuntos infinitos de forma inequvoca, no por extensin, ni por descripcin de sus propiedades, sino explicitando un modo de construir ese conjunto. La definicin recurrente de un conjunto muestra el modo de construir cualquier elemento del mismo a partir de ciertos elementos (ya construidos y llamados elementos base), y de ciertas reglas especficas de construccin. La definicin recurrente de conjuntos contiene dos partes: a) Base: se describen (por comprensin o por extensin) los elementos del conjunto que servirn para crear los otros con la ayuda de las reglas de recurrencia sealadas.

434

Captulo 15. Recursividad (Recurrencia)

b) Recurrencia: se ofrece una formulacin recurrente de las reglas que permiten crear un elemento a partir de elementos ya creados. Por ejemplo, para definir el conjunto de los naturales, basta decir: a) Base: 1 es un nmero natural. b) Recurrencia: El siguiente de un nmero natural es un natural. Es decir, si es natural, entonces es natural.

Otro ejemplo: si queremos definir el conjunto de los enteros impares, bastar la siguiente definicin: a) Base: el entero 1 es impar. b) Recurrencia: Sea impar. Entonces tambin es impar.

Demostracin recurrente de propiedades sobre conjuntos definidos recurrentemente.


Por ltimo, una vez tenemos visto como definir un conjunto mediante recurrencia, y una vez tenemos visto el mtodo de demostracin recurrente, es fcil ofrecer ahora cmo establecer y demostrar distintas propiedades sobre esos conjuntos de definicin recurrente: a) Base: demostrar que la propiedad es cierta para todos los

elementos de la base de definicin del conjunto. b) Recurrencia: La hiptesis de recurrencia consiste en suponer que los elementos que sirven para la creacin de un elemento conjunto verifican la propiedad consiste en demostrar que tambin verifica del . La demostracin de herencia , a partir de la

hiptesis de induccin y de las propiedades de los elementos. Por ejemplo, supongamos que queremos demostrar que el impar mayor que cero (lo llamaremos ) tiene el valor . simo

435

Informtica Aplicada. Programacin en Lenguaje C.

Para demostrarlo, primero verificamos que efectivamente se cumple as en los elementos de la base: el nmero 1. Efectivamente tambin . , es (as y

Y acudimos ahora a la recurrencia: Suponiendo que cierto que ? Teniendo en cuenta que

hemos descrito, por construccin de recurrencia, el conjunto de los impares), es inmediata la demostracin, sin ms que sustituir el valor de : comn al dos que multiplica a y a 1. ; sin ms que sacando factor

Todo lo visto en este apartado del captulo nos permitir tener unas pautas para la construccin de algoritmos o programas recursivos o recurrentes.

Etapas en la construccin de algoritmos recursivos.


Un algoritmo recursivo requiere, como primera providencia, una definicin ms o menos matemtica del problema que se est abordando. Y adems esta especificacin matemtica del problema debe ofrecer las ecuaciones de recurrencia. Para llegar a esa descripcin nos basamos en la definicin recurrente del dominio. Una vez tenemos las relaciones de recurrencia, hay que transcribir esas relaciones a notacin algortmica.

n-1

n+1

Figura 15.2.: Adoquines de la rayuela.

Veamos con un ejemplo cmo podemos estudiar un problema de forma recursiva y pasar por distintas etapas hasta llegar a la construccin de la

436

Captulo 15. Recursividad (Recurrencia)

correspondiente funcin recursiva. El ejemplo es el de la calle adoquinada y la rayuela (cfr. Figura 15.2.). Se considera una calle adoquinada. Cada adoqun est numerado a partir de 0: En esa calle hay nios jugando a la rayuela: se desplazan de un adoqun al siguiente, pero tambin pueden saltar un adoqun. El juego est, pues, basado en estos dos desplazamientos elementales (ver figura 15.3.)

Figura 15.3.: Movimientos de la rayuela. Al principio del juego, los nios se encuentran en el adoqun 0. El problema consiste en calcular el nmero de caminos posibles hasta alcanzar un adoqun prefijado como objetivo.

Para poder realizar una definicin recurrente de nuestro problema, debemos seguir los siguientes pasos: a) En primer lugar debemos establecer cul es el perfil matemtico de la funcin que estamos buscando. A esta funcin la llamaremos Caminos, cuyo parmetro de entrada es un natural n, y la salida en otro natural que indica el nmero de caminos posibles. b) En segundo lugar, necesitamos saber cul ser el enunciado de la funcin (enunciado en comprensin): es el nmero de a partir del caminos posibles que permiten llegar al adoqun

adoqun 0, pasando de un adoqun al siguiente, o saltando uno. c) El siguiente paso es determinar cules son los casos ms simples en los que podemos llamar a nuestra funcin Caminos: Al adoqun 1 se llega de una sola manera: .

437

Informtica Aplicada. Programacin en Lenguaje C.

Al adoqun 2 se llega de dos maneras:

El caso del adoqun 3 ya supone un estudio ms complicado. d) Una vez visto como se describe la funcin y cules son los casos ms elementales, queda descubrir cmo describir el caso general. Para ello debemos basarnos en los desplazamientos elementales. , o bien del Estudiamos tres adoquines consecutivos: para alcanzar el adoqun se puede provenir del adoqun (cfr. Figura 15.4.) adoqun .

n2

n 1

Figura 15.4.: Movimientos de la rayuela para alcanzar el adoqun n . Se divide el conjunto de caminos que llevan al adoqun subconjuntos disjuntos: Los caminos que pasan por el adoqun . . en dos

Los caminos que NO pasan por el adoqun que pasa por

Todo camino a camino que llega a hasta el adoqun

est creado a partir de un . . Todo

al que se la aade el trayecto al que se le aade el trayecto

camino que no pasa por

est creado a partir de uno que llega

Podemos, por tanto, afirmar que los caminos que llegan desde 0 hasta son: .

e) Ya tenemos un enunciado recursivo: . . , .

438

Captulo 15. Recursividad (Recurrencia)

Es evidente, por exigencias del mismo enunciado del problema, que el valor de debe ser siempre mayor que cero y que no se debe invocar a la funcin Caminos con parmetros negativos. Y as lo respeta nuestra definicin recursiva, que toma una valor base no calculado para recursiva a partir de para los valores y y , y solamente acude a la definicin , donde se invoca a la funcin Caminos , ambos mayores que cero.

Adems podemos comprobar que la recurrencia termina: todo valor del dominio inicial provoca un clculo finito. Vamos a demostrarlo por induccin: Base: y terminan ya que no generan ninguna

nueva llamada recursiva. Recurrencia: Supongamos que (para valores de ) y

termina (hiptesis de recurrencia). Eso implica que tambin terminan. Entonces, la llamada y de genera el clculo de

, que ambos

terminan, por la hiptesis de recurrencia. Conclusin: termina para .

Con todo lo hecho, hemos llegado a una correcta definicin de la funcin o mtodo diseado parar calcular de forma recurrente el nmero de caminos posibles para llegar del adoqun cero al adoqun . As, la definicin de la funcin Funcin ( Entero) Entero Constantes Variables Acciones: 1. 2. Si Sino FIN. Entonces . . podra tener el siguiente aspecto:

439

Informtica Aplicada. Programacin en Lenguaje C.

Su cdigo en C tomar, entonces, la siguiente forma: #include <stdio.h> long Caminos(short); int main(void) { short n; printf("Valor de n ... "); scanf( %hd", &n); printf("Caminos posibles: %ld.\n", Caminos(n)); return 0; } long Caminos(short n) { return n < 3 ? n : Caminos(n - 1) + Caminos(n - 2); } Recapitulamos: las etapas seguidas para la elaboracin de la funcin sustentada por un algoritmo recursivo pueden quedar resumidas en las siguientes: a) Especificacin matemtica. Estudio del perfil de la funcin. b) Enunciado en comprensin de la funcin. Un enunciado intuitivo que permita seguir el razonamiento. c) Examen de los casos particulares simples. Entendemos por casos simples aquellos para los cuales el clculo se formula de manera analtica. Son los casos que constituirn la base de la recurrencia. d) Puesta de relieve de un caso general por descubrimiento de una frmula que lo relaciona con el mismo problema basado en una informacin ms pequea, ms prxima al caso de las bases. Este paso es el encargado de buscar la recurrencia: hay que expresar el valor de la funcin en un punto del dominio a partir de uno o varios puntos del dominio. e) Demostracin de la recurrencia. Ese paso ltimo requiere a su vez de tres subpasos:

440

Captulo 15. Recursividad (Recurrencia)

e.1. Comprobacin de que el valor de la funcin puede ser calculado correctamente en cualquier punto o valor del dominio nicamente mediante la aplicacin de las relaciones facilitadas. e.2. Comprobacin de la coherencia de las llamadas de la funcin recursiva. e.3. Comprobacin de la terminacin de la funcin.

rbol de recursin.
Un rbol de recursin sirve para representar las sucesivas llamadas recursivas que un programa recursivo puede generar. Es un concepto simple, pero de gran utilidad prctica para analizar el comportamiento de los algoritmos recursivos diseados. Para construir un rbol, se representa cada llamada a la funcin como un nodo (que dibujaremos en forma de un crculo). Arriba se dibuja el nodo inicial con el que es invocada por primera vez la funcin

Figura 15.5.: rbol de recursin de la funcin Factorial.

441

Informtica Aplicada. Programacin en Lenguaje C.

Figura 15.6.: rbol de recursin de la Rayuela para .

recurrente. Ms all (hacia abajo) se van insertando las llamadas que recibe la funcin recursivamente. En cada nodo del rbol (que se corresponde con una llamada) se le adjunta una etiqueta con el valor o valores de los parmetros de entrada. Por ejemplo, el rbol de recursin de la funcin Factorial tiene el aspecto como el recogido en la figura 15.5. Para la rayuela, el rbol queda recogido en la Figura 15.6. Hemos tomado el caso en que .

Recursin e iteracin.
El concepto de iteracin ya qued visto en el captulo 5, al hablar de los algoritmos y de sus modos de representacin. Y al inicio de ste captulo hemos visto que todos los algoritmos que en el captulo 5 resolvimos mediante la iteracin, tambin pueden ser solventados mediante la recursividad o recurrencia. La cuestin a plantear es entonces: qu es mejor: recursividad, o estructuras iterativas? Para responder a esta cuestin, un factor importante que cabe plantearse es cul de las dos formas requiere

442

Captulo 15. Recursividad (Recurrencia)

menor espacio de almacenamiento en memoria. Y otro, cul de las dos formas resuelve el problema con menor tiempo. Veamos el algoritmo del clculo del factorial, expresado como funcin recurrente (Funcin FactorialR()) y expresado como funcin con estructura iterativa (Funcin FactorialI()) Funcin Constantes Variables Acciones: 1. Si Entonces 2. Sino, Entonces 3. FIN. Funcin Constantes Variables ( Entero) Entero. . . ( Entero) Entero

, , Enteros Acciones: 1. Si , Entonces Sino, Entonces: 1.1. . 1.2. Para Hasta 1.3. . 2. Fin.

. Repetir: .

Aparentemente, parece que la funcin recursiva exige menos memoria, pues de hecho no requiere el uso de ninguna variable local (es decir, variable propia de la funcin) mientras que la funcin iterativa requiere de dos variables. Pero en realidad el programa recursivo va almacenando en una pila de memoria los nmeros: , , , , , que son los parmetros de llamada antes de cada recursin. A medida que vaya saliendo de las sucesivas llamadas, multiplicar esos nmeros en el mismo orden en que lo hace el programa iterativo. As pues, el programa recurrente requiere de mayor almacenamiento. Adems, tambin tardar ms tiempo en ejecutarse, porque debe almacenar en memoria y recuperar

443

Informtica Aplicada. Programacin en Lenguaje C.

luego de ella todos los nmeros, realizar todas las multiplicaciones, y realizar las distintas llamadas a la funcin. Vemoslo sobre la propia funcin en C: long Factorial(short A) { return A ? A * Factorial(A - 1) : 1; } Cada vez que la funcin es invocada por s misma, se crea de nuevo una variable A, distinta de la variable A creada en la anterior invocacin. Para cada llamada creamos un juego de variables cuyo mbito es el de esta llamada, y su vida el tiempo que se tarde en ejecutar la ltima sentencia del bloque de la funcin. Supongamos que queremos conocer el valor del factorial de 3. Invocamos a la funcin Factorial con ese valor como argumento. printf(El factorial de %hd es %ld.\n,3, Factorial(3)); Primera llamada: se crea la variable <A, short, , 3>. Como A es

distinto de cero, no se devuelve el entero 1, sino el producto de A por el Factorial de (A 1). Entonces, antes de terminar la ejecucin de la funcin Factorial y eliminar la variable A localizada en recibir el valor de Factorial de (A 1). Segunda llamada: se crea la variable <A, short, , 2>. Con el mismo necesitamos

nombre que en la llamada anterior, son variables diferentes, ubicadas en posiciones de memoria diferentes. En este momento, la variable en mbito es la ubicada en , la ubicada en no es accesible: siempre que en esta segunda ejecucin de la funcin Factorial hagamos referencia a la variable A, se entiende que nos referimos a la ubicada en . Como esta variable A no vale 0, entonces la funcin devuelve el valor del producto de A (la de localizada en ) por Factorial(A 1). Y de nuevo, antes de terminar la ejecucin de la funcin Factorial y eliminar la variable A necesitamos recibir el valor de Factorial(A 1).

444

Captulo 15. Recursividad (Recurrencia)

Tercera llamada: se crea la variable <A, short,

, 1>. Con el mismo

nombre que en las dos llamadas anteriores, son variables diferentes, ubicadas en posiciones de memoria diferentes. En este momento, la variable en mbito es la ubicada en , las ubicadas en y no son accesibles: siempre que en esta tercera ejecucin de la funcin Factorial hagamos referencia a la variable A, se entiende que nos referimos a la ubicada en . Como esta variable A no vale 0, entonces la funcin ) por Factorial(A 1). Y de devuelve el valor del producto de A (la de la variable A localizada en 1). Cuarta llamada: se crea la variable <A, short, , 0>. Con el mismo

nuevo, antes de terminar la ejecucin de la funcin Factorial y eliminar necesitamos recibir el valor de Factorial(A

nombre que en las tres llamadas anteriores, son variables diferentes, ubicadas en posiciones de memoria diferentes. En este momento, la variable en mbito es la ubicada en ; las ubicadas en , , y no son accesibles: siempre que en esta cuarta ejecucin de la funcin Factorial hagamos referencia a la variable A, se entiende que nos referimos a la ubicada en en . El valor de esta variable es 0 por lo que la funcin devuelve el valor 1 y termina su ejecucin. La variable A ubicada termina su existencia y el control del programa vuelve a quien llam a la funcin. Quien llam a la funcin fue la propia funcin Factorial, en su tercera llamada. Estaba pendiente, para cerrarse y devolver un valor, a recibir un valor de la funcin Factorial. Y ha recibido el valor 1, que multiplica al valor de A que tambin es 1, y devuelve a quien la llam. La variable A ubicada en termina su existencia y el control del programa vuelve a quien llam a la funcin. Y quien llam a la funcin fue la propia funcin Factorial, en su segunda llamada. Estaba pendiente, para cerrarse y devolver un valor, a recibir un valor de la funcin Factorial. Y ha recibido el valor 1, que multiplica al valor de A que es 2, y devuelve a quien la llam. La variable A ubicada

445

Informtica Aplicada. Programacin en Lenguaje C.

en

termina su existencia y el control del programa vuelve a quien

llam a la funcin. Y quien llam a la funcin fue la propia funcin Factorial, en su primera llamada. Estaba pendiente, para cerrarse y devolver un valor, a recibir un valor de la funcin Factorial. Y ha recibido el valor 2, que multiplica al valor de A que es 3, y devuelve a quien la llam. La variable A ubicada en termina su existencia y el control del programa vuelve a quien llam a la funcin. Y quien llam a la funcin Factorial fue la funcin principal, que vuelve a recuperar el control de ejecucin del programa y que recibe el valor devuelto por la funcin que se lo pasa como parmetro a la funcin printf para que muestre ese valor por pantalla: El factorial de 3 es 6. Cuatro ejecuciones, cuatro mbitos, cuatro variables distintas, cuatro vidas distintas. Veamos ahora un segundo ejemplo: el de la funcin euclides. En esta funcin la recurrencia es an ms clara, puesta el propio algoritmo de Euclides es autodefinido. El cdigo de la funcin podra quedar as: long euclides(long a, long b) { return b ? euclides(b, a % b) : a; } No vamos a comentar su ejecucin con tanta largueza como en la funcin Factorial. Pero as escrita, la funcin euclides se comporta tal y como qued definido el algoritmo en el captulo 8. All decamos: Euclides, matemtico del siglo V a. de C. present un algoritmo muy fcil de implementar, y de muy bajo coste computacional. El algoritmo de Euclides dice que el mximo comn divisor de dos enteros (diremos donde con ), donde es igual a donde y , entendiendo por y y

el resto de la divisin de

. Y el proceso puede seguirse hasta llegar a unos valores de

446

Captulo 15. Recursividad (Recurrencia)

que verifiquen que

, y

. Entonces, el algoritmo de

Euclides afirma que, llegado a estos valores el valor buscado es Y eso es precisamente lo que se ha implementado en esta funcin. Otro ejemplo para estudiar el comportamiento de una funcin definida de forma recursiva o de forma iterativa lo tenemos con los nmeros de Fibonacci. Mostramos de nuevo la definicin recursiva (Funcin FibonacciR) y la definicin iterativa (Funcin FibonacciI). Funcin Constantes Variables Acciones: 1. Si Entonces Si no, Entonces, 2. Fin. Funcin Constantes ( Entero) Entero . . ( Entero) Entero

Variables , , e , Enteros. Acciones: 1. Si Entonces 2. Si no, Entonces, 2.1. . 2.2. 2.3. Para Hasta 2.3.1. 2.3.2. 2.3.3. 2.4. 3. FIN.

, Hacer:

El rbol de recursividad para la funcin

, para

queda de

forma idntica al rbol antes presentado en la Figura 15.6. para el problema de la Rayuela. Y as observamos que para el clculo del valor de hemos calculado una vez el valor de , tres veces el de , dos y dos veces veces el valor de

447

Informtica Aplicada. Programacin en Lenguaje C.

el de

. As pues, con el rbol de recursin vemos que el

programa recursivo repite innecesariamente los mismos clculos una y otra vez. Y el tiempo que tarda la funcin recursiva en calcular aumenta de forma exponencial a medida que aumenta .

En cambio, la funcin iterativa usa un tiempo de orden lineal (aumenta linealmente con el incremento del valor de notable. La recursin siempre puede reemplazarse con la iteracin y las pilas de almacenamiento de datos. La pregunta es, entonces: Cundo usamos recursin, y cundo usamos iteracin? Si el rbol de recursin tiene muchas ramas, con poca duplicacin de tareas, entonces el mtodo ms adecuado es la recursin, siempre que adems el programa resultante sea ms claro, sencillo y fcil de obtener. Los algoritmos recursivos son muy apropiados cuando el problema a resolver, la funcin a calcular o la estructura de datos a procesar estn definidos en trminos recursivos. La recursin debe tenerse en cuenta como una tcnica que nos permite disear algoritmos de una forma sencilla, clara y elegante. No se debe utilizar por tanto cuando dificulte la comprensin el algoritmo. ), de forma que la diferencia de tiempos entre una y otra forma de implementacin es

Ejercicio: las torres de Hanoi.


Dice la leyenda que, al crear el mundo, Dios situ sobre la Tierra tres varillas de diamante y sesenta y cuatro discos de oro. Los discos eran todos de diferente tamao, e inicialmente fueron colocados en orden decreciente de dimetros sobre la primera de las varillas. Tambin cre Dios un monasterio cuyos monjes tenan la tarea de trasladar todos los discos desde la primera varilla a la tercera. La nica operacin permitida a los monjes para llevar a cabo su tarea era mover un nico disco cada

448

Captulo 15. Recursividad (Recurrencia)

vez, de una varilla a otra cualquiera, con la condicin de que no se poda situar encima de un disco otro de dimetro mayor. La leyenda deca tambin que cuando los monjes terminasen su tarea, el mundo llegara a su fin. La leyenda no es cierta: es fruto de la imaginacin de un matemtico del siglo XVIII, que invent el juego de las Torres de Hanoi (as se llama) y que dise as su propia campaa de publicidad y marketing que le permitiera lanzar su invento al mercado. La invencin result, sin embargo, efectiva: ha perdurado hasta nuestros das, el juego es conocido en todo el mundo, y nos ha dejado una pregunta: si la leyenda fuera cierta cundo sera el fin del mundo?

El mnimo nmero de movimientos que se necesitan para resolver este problema es de . Si los monjes hicieran un movimiento por segundo, y no parasen en ningn instante hasta completar la total ejecucin de la tarea de traslado, los discos estaran en la tercera varilla en poco menos de 585 mil millones de aos. Teniendo en cuenta que la tierra tiene slo unos cinco mil millones, o que el universo est entre quince y veinte mil millones de aos, est claro que o los monjes se dan un poco ms de prisa, o tenemos universo y mundo para rato.

449

Informtica Aplicada. Programacin en Lenguaje C.

Se puede generalizar el problema de Hanoi variando el nmero de discos: desde un mnimo de 3 hasta el mximo que se quiera. El problema de Hanoi es curioso, y su formulacin elegante. Y su solucin es muy rpida de calcular. La pega no est en la complicacin del procedimiento (que no es complicado), sino en que a medida que aumenta el nmero de discos crece exponencialmente el nmero de pasos. Existe una versin recursiva eficiente para hallar el problema de las torres de Hanoi para pocos discos; pero el coste de tiempo y de memoria se incrementa excesivamente al aumentar el nmero de discos. La solucin recursiva es llamativamente sencilla de implementar y de construir. Y de una notable belleza, creo yo. Si numeramos los discos desde 1 hasta los discos, e manera: 1. Si N = 1, Entonces Trasladar disco de X a Z. Sino 1.1. Trasladar 1 .. N 1 de X z Y, (auxiliar: Z). 1.2. Trasladar N de X a Z. 1.3. Trasladar 1 .. N de Y a Z (auxiliar: X) 2. Fin. Podemos definir una funcin que muestre por pantalla los movimientos necesarios para hacer el traslado de la torre de un poste a otro. A esa funcin la llamaremos valores: a) una variable entera que indica el disco ms pequeo de la pila que se quiere trasladar. , y recibe como parmetros los siguientes , y si llamamos al poste

donde quedan inicialmente colocado los discos,

al poste de destino de

al tercer poste intermedio, el algoritmo recurrente que

resuelve el problema de las torres de Hanoi quedara de la siguiente

450

Captulo 15. Recursividad (Recurrencia)

b) una variable entera que indica el disco ms grande de la pila que se desea trasladar. (Hay que tener en cuenta que con el algoritmo que hemos definido siempre se trasladan discos de tamaos consecutivos.) c) Una variable que indique el poste donde estn los discos que trasladamos. d) Una variable que indica el poste a donde se dirigen los discos. Un modo de identificar los postes y de tener en todo momento identificado el poste auxiliar con variables enteras es el siguiente: Llamaremos al poste origen poste exigimos a las dos variables , . Y entonces siempre tendremos que el poste auxiliar vendr siempre definido con la expresin , como puede verse en la Tabla 15.2. , y al poste destino poste . Y ,e las siguientes restricciones:

1 1 2 2 3 3

2 3 1 3 1 2

3 2 3 1 2 1

Tabla 15.2.: Postes de las torres de Hanoi.

La funcin Funcin Constantes Variables Acciones: 1. Si 2. Sino (

queda entonces de la siguiente forma: , , , , Enteros)

, Entonces [Mostrar] Mover disco D1 de

a .

451

Informtica Aplicada. Programacin en Lenguaje C.

2.1. 2.2. 2.3. 3. FIN.

. . .

La forma que adquiere esta funcin implementada en C es tan sencilla como lo que se muestra a continuacin: typedef unsigned short int usi; void Hanoi(usi D1, usi D2, usi i, usi j) { if(D1 == D2) printf("Disco %hu de %hu a %hu\n",D1, i, j); else { Hanoi(D1, D2 - 1, i, 6 - i - j); Hanoi(D2, D2, i, j); Hanoi(D1, D2 - 1, 6 - i - j, j); } } Y si se invoca esta funcin inicialmente con la sentencia Hanoi(1, discos, 1, 3); donde discos es el nmero de discos que tiene la torre de Hanoi, la ejecucin de esta funcin muestra todos los movimientos que debe hacerse con los anillos para pasarlos desde el poste 1 al poste 3. Por ejemplo, si tomamos el valor discos = 4 tendremos la siguiente salida: Disco Disco Disco Disco Disco Disco Disco Disco Disco Disco Disco Disco Disco Disco Disco 1 2 1 3 1 2 1 4 1 2 1 3 1 2 1 de de de de de de de de de de de de de de de 1 1 2 1 3 3 1 1 2 2 3 2 1 1 2 a a a a a a a a a a a a a a a 2 3 3 2 1 2 2 3 3 1 1 3 2 3 3

452

Captulo 15. Recursividad (Recurrencia)

Para terminar esta presentacin de las torres de Hanoi queda pendiente una cosa: demostrar que, efectivamente, el nmero de movimientos para desplazar todos los discos de un poste a otro con las condiciones impuestas en el juego es igual a dos elevado al nmero de discos, menos uno. Eso se demuestra fcilmente por induccin (como no): Base: Se verifica para (movemos un solo disco):

: efectivamente, el mnimo nmero de movimientos es uno. Recurrencia: Supongamos que se cumple para Entonces si tenemos veces los decir, (c.q.d.) tenemos : . , es

discos, lo que hacemos es desplazar dos

primeros discos y entre medias una vez el disco

Recapitulacin.
Hemos visto el modelo recursivo para plantear y solventar problemas. Esta forma de trabajo es especialmente til cuando pretendemos solventar cuestiones basadas en propiedades definidas sobre el conjunto de enteros o un conjunto numerable donde se pueda establecer una correspondencia entre los enteros y los elementos de ese conjunto. Hemos visto el modo en que se definen por recurrencia una serie de conjuntos, cmo se demuestran propiedades para estos conjuntos basndonos en la demostracin por induccin, y cmo se llega as a replantear muchos problemas, alcanzando una formulacin muy elegante y sencilla: la forma recursiva. La recursividad ofrece habitualmente algoritmos muy sencillos de implementar y de interpretar. La principal limitacin que presenta esta forma de resolver problemas es que habitualmente supone un elevado

453

Informtica Aplicada. Programacin en Lenguaje C.

coste de tiempo de computacin y de capacidad de almacenamiento de memoria. Hemos descrito el camino a seguir para alcanzar una solucin recurrente ante un problema que nos planteamos. Los ejercicios que planteamos a continuacin pueden servir para seguir profundizando en ese itinerario por el que se llega a las definiciones recurrentes.

Ejercicios.

15.1.

Definir la operacin potencia de forma recursiva.

Simplemente, basta considerar que estas dos conocidas expresiones tenemos ya recurrencia.

, y que una base

. Con y una

As, el cdigo de la funcin recurrente tendr un aspecto similar al que hemos visto para la funcin factorial: #include <stdio.h> long potencia(short, short); int main(void) { short b, e; printf("Valor scanf( %hd", printf("Valor scanf( %hd", de la base ...... "); &b); del exponente ... "); &e);

printf("%hd elevado a %hd es: ", b, e); printf("%ld\n, potencia(b, e)); return 0; } long potencia(short b, short e)

454

Captulo 15. Recursividad (Recurrencia)

{ return e ? b * potencia(b, e - 1) : (long)1; } La funcin, como se ve, devuelve el valor 1 si el exponente es cero (si es falso); y devuelve el producto de la base por la potencia disminuido en uno el exponente si el exponente es distinto de cero (verdadero).

15.2.

Definir la operacin suma de enteros de forma recursiva. Tngase en cuenta, por ejemplo, que si pretendemos sumar dos enteros a y b, podemos tomar: suma(a,b) = a si b es igual a cero; y suma(a,b) es 1 + suma(a,b - 1) en otro caso.

Ya tenemos, en el mismo enunciado, la definicin de la funcin. Simplemente ser algo de este estilo: long suma(short a, short b) { return b ? 1 + suma(a , b - 1) : (long)a; } Evidentemente, para que el programa est completo faltara la

declaracin de la funcin y la definicin de la funcin principal. No es necesario incluir este cdigo aqu, en la resolucin de todos estos ejercicios. S deber escribirlo usted si desea verificar que estas funciones realizan la operacin que exiga el enunciado.

15.3.

Definir la operacin producto de forma recursiva. Tngase en cuenta, por ejemplo, que si pretendemos multiplicar dos enteros a y b, podemos tomar: cero; y es si es igual a en otro caso.

455

Informtica Aplicada. Programacin en Lenguaje C.

Otra vez ha quedado la funcin completamente definida en el mismo enunciado del problema. Un posible cdigo de la funcin podra ser el siguiente: long producto(short a, short b) { return b ? a + producto(a, b - 1) : 0; }

15.4.

Definir

una

funcin

que

muestre,

mediante

un

proceso

recurrente, el cdigo binario de un entero dado. Convendr recordar lo explicado en captulos anteriores sobre codificacin binaria.

Para encontrar este proceso es sencillo buscar los casos ms sencillos, que forman la Base de la recurrencia: El cdigo binario de cdigo binario de es 1. es 0; el

Para el proceso de recurrencia baste con decir que si el nmero es mayor que 1, entonces ya necesita de al menos dos dgitos binarios; y si el nmero es mayor que 3 entonces requiere ya de al menos tres dgitos. Y, en general, si el nmero es mayor que nmero requiere, para su codificacin binaria, de al menos entonces el dgitos.

Y, por lo indicado en los captulos sobre codificacin numrica y sobre codificacin interna de la informacin, sabemos que podemos concatenar dgitos a fuerza de dividir por dos sucesivas veces hasta llegar al valor 0 1. Podemos entonces definir el siguiente algoritmo recursivo para obtener el cdigo binario de un entero: Funcin Constantes Variables ( Entero en base 10) Muestra Cdigo binario

456

Captulo 15. Recursividad (Recurrencia)

Acciones: 1. Si Entonces [Mostrar dgito] Mostrar . Si no [Es decir, ] Entonces 1.1. . 1.2. [Mostrar digito] Mostrar . 2. Fin. Cuyo cdigo en C tiene el siguiente aspecto: void Binario(unsigned long n) { if(n == 1) printf("%ld",n); else { Binario(n/2); printf("%ld",n % 2); } } Supongamos que invocamos a la funcin Binario con el valor para el parmetro n igual a 12. Llamada Binario(12). Profundidad de Recursin: 1. Como n = 12 no es 1, se ejecuta Binario(6). Llamada Binario(6). Profundidad de Recursin: 2. Como n = 6 no es 1, se ejecuta Binario(3). Llamada Binario(3). Profundidad de Recursin: 3. Como n = 3 no es 1, se ejecuta Binario(1). Llamada Binario(1). Profundidad de Recursin: 4. Como n = 1 simplemente imprimimos ese valor 1 y salimos de la ejecucin. Pasamos a la Profundidad de Recursin 3. La segunda sentencia, despus de la llamada Binario(1) que ya ha finalizado es la de imprimir el resto de dividir N = 3 por 2: 1. Se termina as la ejecucin de Binario(3). Pasamos a la Profundidad de Recursin 2. La segunda sentencia, despus de la llamada Binario(3) que ya ha finalizado es la de imprimir el resto de dividir N = 6 por 2: 0. Se termina as la ejecucin de Binario(6). Pasamos a la Profundidad de Recursin 1. La segunda sentencia, despus de la llamada Binario(6) que ya ha finalizado es la de imprimir el resto de dividir N = 12 por 2: 0. Se termina as la ejecucin

457

Informtica Aplicada. Programacin en Lenguaje C.

de Binario(12). Vuelve el control a la funcin que invoco inicialmente la funcin Binario con el valor 12. Han quedado impreso, y por ese orden, los dgitos 1, 1, 0 y 0, que son, efectivamente, el cdigo binario del entero 12 (1100).

15.5.

Vea la definicin de la funcin cuyo prototipo es void f(char*, int); Y su definicin es: void f( char *cad, int i) { if( cad[i] != '\0' ) { f(cad,i+1); printf("%c", cad[i]); } } Suponga que la invocamos con los siguientes valores de entrada: f(EXAMEN, 0); Indique entonces qu salida ofrece esta funcin por pantalla.

La funcin f recibe una cadena de texto y el ndice de uno de sus elementos. Mientras que el valor del elemento de la cadena en ese ndice no sea el carcter nulo, la funcin f volver a invocarse a s misma, incrementando en uno el ndice. Cuando llegue al carcter nulo no hace ms llamadas. En ese momento, se ejecuta la segunda lnea de cada una de las llamadas a la funcin (la lnea con el printf), y se van imprimiendo todos los caracteres en el orden inverso en que se han pasado sus ndices en las sucesivas llamadas.

458

Captulo 15. Recursividad (Recurrencia)

i = 0 a[i] = h i = 1 a[i] = o i = 2 a[i] = l i = 3 a[i] = a i = 4 a[i] = \0 Figura 15.7.: rbol de recursin para el cdigo del ejercicio 14.5. Por ejemplo, supongamos que invocamos a la funcin f con una cadena, declarada como char a[] = hola; y el valor de ndice 0: f(a, 0); Si hacemos el rbol de recursin, obtenemos el recogido en la figura 15.7. En ella ha quedado indicado el valor del ndice i y el correspondiente valor en esa posicin de la cadena de caracteres. Cuando llega a la llamada con el valor de i = 4, al encontrarse con que a[i] es, efectivamente, el carcter nulo, no realiza una nueva llamada a s misma ni ejecuta la funcin printf. Entonces queda ejecutar la segunda sentencia recogida en la estructura condicional de la llamada anterior, con i = 3: imprime por tanto el carcter a y se termina la ejecucin de esta llamada a la funcin, devolviendo el control a la anterior llamada: aquella que se hizo con el valor de i = 2, y entonces imprime el carcter l y se termina de nuevo la ejecucin de esta llamada a la funcin, devolviendo el control a la anterior llamada: aquella que se hizo con el valor de i = 1, y entonces imprime el carcter o y se termina de nuevo la ejecucin de esta llamada a la funcin, devolviendo el control a la anterior llamada: aquella que se hizo con el valor de i = 0, y entonces imprime el carcter h y se termina de nuevo la ejecucin de esta llamada a la funcin, devolviendo el control a la funcin principal o a aquella que invoc originariamente a la funcin f.

459

Informtica Aplicada. Programacin en Lenguaje C.

La salida es, por lo tanto, si la entrada es EXAMEN: NEMAXE: la cadena impresa en orden inverso.

15.6.

Definir una funcin que devuelva el mayor de una lista de nmeros. El algoritmo no puede comparar en ningn momento ms de dos nmeros para decidir el mayor de entre todos los de la lista.

Vamos a definir una funcin, que llamaremos

, y que recibe como

parmetros una lista de enteros y un valor entero que indica la cantidad de elementos de esa lista. La funcin devuelve un entero: el mayor de entre todos los valores de la lista. La limitacin impuesta en el enunciado nos indica la base de nuestro algoritmo de recurrencia: Si la lista tiene un solo elemento, ese elemento es el que devuelve la funcin; si la lista tiene dos elementos, la funcin devuelve el mayor de ellos. Si la lista tiene ms de dos elementos, entonces la funcin no puede contestar a la cuestin, porque en ningn momento podemos comparar ms de dos elementos. Supongamos que tenemos elementos en la lista. Entonces est claro segundos. Esta es la base para definir

que el mayor de entre ellos es igual al mayor entre el mayor de los primeros y el mayor entre los nuestro proceso de recurrencia: Podemos entonces plantear el algoritmo de la siguiente manera: Funcin M ( Constantes Enteros, Entero) Entero

Variables , e , Enteros. Acciones: 1. Si n = 1 Entonces Si no, Entonces 1.1. .

460

Captulo 15. Recursividad (Recurrencia)

1.2. 1.3. 1.4. Si 2. Fin.

. . Entonces Si no [es decir, . ] Entonces .

La funcin, implementada en C, tendra un aspecto como el que sigue: short M(short *v, short n) { short m; short x, y; if(n == 1) return *v; m = n / 2; x = M(v , m); y = M(v + m , n - m); return x > y ? x: y; } Y la llamada a esta funcin o al mtodo es de la forma M(v, dim); donde v es una variable de tipo vector o array de enteros y donde dim es el nmero de elementos del vector v.

461

Informtica Aplicada. Programacin en Lenguaje C.

462

CAPTULO 16
PUNTEROS.
La memoria puede considerarse como una enorme cantidad de

posiciones de almacenamiento de informacin perfectamente ordenadas. Cada posicin es un octeto o byte de informacin. Cada posicin viene identificada de forma inequvoca por un nmero que suele llamarse direccin de memoria. Cada posicin de memoria tiene una direccin nica. Los datos de nuestros programas se guardan en esa memoria. La forma en que se guardan los datos en la memoria es mediante el uso de variables. Una variable es un espacio de memoria reservado para almacenar un valor: valor que pertenece a un rango de valores posibles. Esos valores posibles los determina el tipo de dato de esa variable. Dependiendo del tipo de dato, una variable ocupar ms o menos bytes de la memoria, y codificar la informacin de una u otra manera. Si, por ejemplo, creamos una variable de tipo float, estaremos reservando cuatro bytes de memoria para almacenar sus posibles valores. Si, por ejemplo, el primero de esos bytes es el de posicin

Informtica Aplicada. Programacin en Lenguaje C.

ABD0:FF31 (es un modo de escribir: en definitiva estamos dando 32 bits para codificar las direcciones de la memoria), el segundo byte ser el ABD0:FF32, y luego el ABD0:FF33 y finalmente el ABD0:FF34. La direccin de memoria de esta variable es la del primero de sus bytes; en este caso, diremos que toda la variable float est almacenada en ABD0:FF31. Ya se entiende que al hablar de variables float, se emplean un total de 4 bytes. Ese es el concepto habitual cuando se habla de la posicin de memoria o de la direccin de una variable. Adems de los tipos de dato primitivos ya vistos y usados ampliamente en los captulos anteriores, existe un C un tipo de dato especial, que ofrece muchas posibilidades y confiere al lenguaje C de una filosofa propia. Es el tipo de dato puntero. Mucho tiene que ver ese tipo de dato con la memoria de las variables. Este captulo est dedicado a su presentacin.

Definicin y declaracin.
Una variable tipo puntero es una variable que contiene la direccin de otra variable. Para cada tipo de dato, primitivo o creado por el programador, permite la creacin de variables puntero hacia variables de ese tipo de dato. Existen punteros a char, a long, a double, etc. Son nuevos tipos de dato: puntero a char, puntero a long, puntero a double, Y como tipos de dato que son, habr que definir para ellos un dominio y unos operadores. Para declarar una variable de tipo puntero, la sintaxis es similar a la empleada para la creacin de las otras variables, pero precediendo al nombre de la variable del carcter asterisco (*). tipo *nombre_puntero;

464

Captulo 16. Punteros.

Por ejemplo: short int *p; Esa variable p as declarada ser una variable puntero a short, que no es lo mismo que puntero a float, etc. En una misma instruccin, separados por comas, pueden declararse variables puntero con otras que no lo sean: long a, b, *c; Se han declarado dos variables de tipo long y una tercera que es puntero a long.

Dominio y operadores para los punteros.


El dominio de una variable puntero es el de las direcciones de memoria. En un PC las direcciones de memoria se codifican con 32 bits (4 bytes), y toman valores desde 0000:0000 hasta FFFF:FFFF, (expresados en base hexadecimal). El operador sizeof, aplicado a cualquier variable de tipo puntero, devuelve el valor 4. Una observacin importante: si un PC tiene direcciones de 32 bytes cunta memoria puede llegar a direccionar?: Pues con 32 bits es posible codificar hasta Giga bytes de memoria. Pero sigamos con los punteros. Ya tenemos el dominio. Codificar, en un formato similar al de los enteros largos sin signo, las direcciones de toda nuestra memoria. Ese ser su dominio de valores. Los operadores son los siguientes: Operador direccin (&): Este operador se aplica a cualquier variable, y devuelve la direccin de memoria de esa variable. Por ejemplo, se puede escribir: bytes, es decir, hasta bytes, es decir 4

465

Informtica Aplicada. Programacin en Lenguaje C.

long x, *px; px = &x; Y as se ha creado una variable puntero a long llamada px, que servir para almacenar direcciones de variables de tipo long. Mediante la segunda instruccin asignamos a ese puntero la direccin de la variable x. Habitualmente se dice que px apunta a x. El operador direccin no es propio de los punteros, sino de todas las variables. Pero no hemos querido presentarlo hasta el momento en que por ser necesario creemos que tambin ha de ser fcilmente comprendido. De hecho este operador ya lo usbamos, por ejemplo, en la funcin scanf, cuando se le indica a esa funcin dnde queremos que almacene el dato que introducir el usuario por teclado: por eso, en esa funcin precedamos el nombre de la variable cuyo valor se iba a recibir por teclado con el operador &. Hay una excepcin en el uso de este operador: no puede aplicarse sobre una variable que haya sido declarada register (cfr. Captulo 12). El motivo es claro: al ser una variable register, le hemos indicado al compilador que no almacene su informacin en la memoria sino en un registro de la ALU. Y si la variable no est en memoria, no tiene sentido que le solicitemos la direccin de donde no est. Operador indireccin (*): Este operador slo se aplica a los punteros. Al aplicar a un puntero el operador indireccin, se obtiene el contenido de la posicin de memoria apuntada por el puntero. Supongamos: float pi = 3.14, *pt; pt = &pi; Con la primera instruccin, se han creado dos variables: <pi, float, , 3.14> y <pt, float*, , ? >

Con la segunda instruccin damos valor a la variable puntero: <pt, float*, , >

466

Captulo 16. Punteros.

Ahora la variable puntero pt vale la variable pi.

, que es la direccin de memoria de

Hablando ahora de la variable pt, podemos hacernos tres preguntas, todas ellas referidas a direcciones de memoria. 1. Dnde est pt? Porque pt es una variable, y por tanto est ubicada en la memoria y tendr una direccin. Para ver esa direccin, basta aplicar a pt el operador direccin &. pt est en &pt. Tendremos que la variable pt est en .

2. Qu vale pt? Y como pt es un puntero, pt vale o codifica una determinada posicin de memoria. Su valor pertenece al dominio de direcciones y est codificado mediante 4 bytes. En concreto, pt vale la direccin de la variable pi. pt vale &pi. Tendremos, por tanto, que pt vale .

3. Qu valor est almacenado en esa direccin de memoria a donde apunta pt? Esta es una pregunta muy interesante, donde se muestra la gran utilidad que tienen los punteros. Podemos llegar al valor de cualquier variable tanto si disponemos de su nombre como si disponemos de su direccin. Podemos llegar al valor de la posicin de memoria apuntada por pt, que como es un puntero a float, desde el puntero tomar ese byte y los tres siguientes como el lugar donde se aloja una variable float. Y para llegar a ese valor, disponemos del operador indireccin. El valor codificado en la posicin almacenada en pt es el contenido de pi: *pt es 3.14. Al emplear punteros hay un peligro de confusin, que puede llevar a un inicial desconcierto: al hablar de la direccin del puntero es fcil no entender si nos referimos a la direccin que trae codificada en sus cuatro bytes, o la posicin de memoria dnde estn esos cuatro bytes del puntero que codifican direcciones. Es muy importante que las variables puntero estn correctamente direccionadas. Trabajar con punteros a los que no se les ha asignado

467

Informtica Aplicada. Programacin en Lenguaje C.

una direccin concreta conocida (la direccin de una variable) es muy peligroso. En el caso anterior de la variable pi, se puede escribir: *pt = 3.141596; y as se ha cambiado el valor de la variable pi, que ahora tiene algunos decimales ms de precisin. Pero si la variable pt no estuviera correctamente direccionada mediante una asignacin previa en qu zona de la memoria se hubiera escrito ese nmero 3.141596? Pues en la posicin que, por defecto, hubiera tenido esos cuatro bytes que codifican el valor de la variable pt: quiz una direccin de otra variable, o a mitad entre una variable y otra; o en un espacio de memoria no destinado a almacenar datos, sino instrucciones, o el cdigo del sistema operativo, En general, las consecuencias de usar punteros no inicializados, son catastrficas para la buena marcha de un ordenador. Detrs de un programa que cuelga al ordenador, muchas veces hay un puntero no direccionado. Pero no slo hay que inicializar las variables puntero: hay que inicializarlas bien, con coherencia. No se puede asignar a un puntero a un tipo de dato concreto la direccin de una variable de un tipo de dato diferente. Por ejemplo: float x, *px; long y; px = &y; Si ahora hacemos referencia a *px trabajaremos la informacin de la variable y como long, o como float? Y peor todava: float x, *px; char y; px = &y; Al hacer referencia a *px leemos la informacin del byte cuya direccin es la de la variable y, o tambin se va a tomar en consideracin los otros tres bytes consecutivos? Porque la variable px considera que apunta a variables de 4 bytes, que pasa eso es un

468

Captulo 16. Punteros.

puntero a float. Pero la posicin que le hemos asignado es la de una variable tipo char, que nicamente ocupa un byte. El error de asignar a un puntero la direccin de una variable de tipo de dato distinto al puntero est, de hecho, impedido por el compilador, y si encuentra una asignacin de esas caractersticas, aborta el proceso de compilacin.

Punteros y vectores.
Los punteros sobre variables simples tienen una utilidad clara en las funciones. All los veremos con detenimiento. Lo que queremos ver ahora es el uso de punteros sobre arrays. Un array, o vector, es una coleccin de variables, todas del mismo tipo, y colocadas en memoria de forma consecutiva. Si creamos una array de cinco variables float, y el primero de los elementos queda reservado en la posicin de memoria FF54:AB10, entonces no cabe duda que el segundo estar en FF54:AB14 (FF54:AB10 + 4), y el tercero en FF54:AB18 (FF54:AB10 + 8), y el cuarto en FF54:AB1C (FF54:AB10 + 12) y el ltimo en FF54:AB20 (FF54:AB10 + 16): tenga en cuenta que las variables de tipo float ocupan 4 bytes. Supongamos la siguiente situacin: long a[10]; long *pa; pa = &a[0]; Y con esto a la vista, pasamos ahora a presentar otros dos operadores, muy usados con los punteros. Operador incremento (++) y decremento (): Estos operadores no son nuevos, y ya los conocemos. Todas sus propiedades que se vieron con los enteros siguen vigentes ahora con las direcciones de memoria.

469

Informtica Aplicada. Programacin en Lenguaje C.

Desde luego, si el incremento es sobre el contenido de la variable apuntada sobre el puntero (*pa)++;, no hay nada que aadir: se incrementar el contenido de esa variable apuntada, de la misma forma que si el operador estuviera aplicado sobre la variable apuntada misma: es lo mismo que escribir a[0]++;. Nos referimos a incrementar el valor del puntero. Qu sentido tiene incrementar en 1 una direccin de memoria? El sentido ser el de acudir al siguiente valor situado en la memoria. Y si el puntero es de tipo long, y apunta a variables long, entonces lo que se espera cuando se incremente un 1 ese puntero es que su valor codificado se incremente en 4: porque 4 es el nmero de bytes que deberemos saltar para pasar de apuntar a una variable long a pasar a apuntar a otra variable del mismo tipo almacenada de forma consecutiva. Es decir, que si pa contiene la direccin de a[0], entonces pa + 1 es la direccin del elemento a[1], y pa + 9 es la direccin del elemento a[9]. Y en el siguiente ejemplo, tenemos:

long a[10], *pa = &a[0]; a F6C3:9870 F6C3:9871 F6C3:9872 F6C3:9873 F6C3:9874 F6C3:9875 F6C3:9876 F6C3:9877 F6C3:9878 F6C3:9879 F6C3:987A F6C3:987B F6C3:987C F6C3:987D

short b[10], *pb = &b[0]; b F6C3:9870 F6C3:9871 F6C3:9872 F6C3:9873 F6C3:9874 F6C3:9875 F6C3:9876 F6C3:9877 F6C3:9878 F6C3:9879 F6C3:987A F6C3:987B F6C3:987C F6C3:987D

pa

pb pb + 1

b[0] b[1] b[2] b[3] b[4] b[5]

a[0]

pa + 1

pb + 2 pb + 3

a[1]

pa + 2

pb + 4 pb + 5

a[2]

pa + 3

pb + 6

470

Captulo 16. Punteros.

Al ir aumentando el valor del puntero, nos vamos desplazando por los distintos elementos del vector, de tal manera que hablar de a[0] es lo mismo que hablar de *pa; y hablar de a[1] es lo mismo que hablar de *(pa + 1); y, en general, hablar de a[i] es lo mismo que hablar de *(pa + i). Y lo mismo si comentamos el ejemplo de las variables de tipo short. La operatoria o aritmtica de punteros tiene en cuenta el tamao de las variables que se recorren. En el siguiente programa, y en la salida que ofrece por pantalla, se puede ver este comportamiento de los punteros. Sea cual sea el tipo de dato del puntero y de la variable a la que apunta, si calculo la resta entre dos punteros situados uno al primer elemento de un array y el otro al ltimo, esa diferencia ser la misma, porque la resta de direcciones indica cuntos elementos de este tipo hay (caben) entre esas dos direcciones. En nuestro ejemplo, todas esas diferencias valen 9. Pero si lo que se calcula es el nmero de bytes entre la ltima posicin (apuntada por el segundo puntero) y la primera (apuntada por el primer puntero), entonces esa diferencia s depender del tamao de la variable del array. #include <stdio.h> int main(void) { char c[10], *pc1, *pc2; short h[10], *ph1, *ph2; float f[10], *pf1, *pf2; double d[10], *pd1, *pd2; long double ld[10], *pld1, *pld2; pc1 = &c[0]; ph1 = &h[0]; pf1 = &f[0]; pd1 = &d[0]; pld1 = &ld[0]; pc2 = &c[9]; ph2 = &h[9]; pf2 = &f[9]; pd2 = &d[9]; pld2 = &ld[9];

printf(" pc2(%p) - pc1(%p) = %hd\n",pc2,pc1,pc2 - pc1); printf(" ph2(%p) - ph1(%p) = %hd\n",ph2,ph1,ph2 - ph1); printf(" pf2(%p) - pf1(%p) = %hd\n",pf2,pf1,pf2 - pf1); printf(" pd2(%p) - pd1(%p) = %hd\n",pd2,pd1,pd2 - pd1); printf("pld2(%p) - pld1(%p) = %hd\n",pld2,pld1,pld2 - pld1); printf("\n\n"); printf("(long)pc2-(long)pc1=%3ld\n",(long)pc2-(long)pc1);

471

Informtica Aplicada. Programacin en Lenguaje C.

printf("(long)ph2-(long)ph1=%3ld\n",(long)ph2-(long)ph1); printf("(long)pf2-(long)pf1=%3ld\n",(long)pf2-(long)pf1); printf("(long)pd2-(long)pd1=%3ld\n",(long)pd2-(long)pd1); printf("(long)pld2-(long)pld1=%3ld\n",(long)pld2-(long)pld1); return 0; } Que ofrece, por pantalla, el siguiente resultado: pc2(0012FF89) ph2(0012FF62) pf2(0012FF4C) pd2(0012FF20) pld2(0012FECE) (long)pc2 (long)ph2 (long)pf2 (long)pd2 (long)pld2 - pc1(0012FF80) - ph1(0012FF50) - pf1(0012FF28) - pd1(0012FED8) - pld1(0012FE74) = = = = = 9 18 36 72 90 = = = = = 9 9 9 9 9

- (long)pc1 - (long)ph1 - (long)pf1 - (long)pd1 - (long)pld1

Que hemos de interpretar bien y entender. Repetimos: al calcular la diferencia entre el puntero que apunta al noveno elemento de la matriz y el que apunta al elemento cero, en todos los casos el resultado ha de ser 9: porque en la operatoria de punteros, independientemente del tipo del puntero, lo que se obtiene es el nmero de elementos que hay entre las dos posiciones de memoria sealadas. Al convertir las direcciones en valores tipo long, ya no estamos calculando cuntas variables hay entre ambas direcciones, sino la diferencia entre el valor que codifica la ltima posicin del vector y el valor que codifica la primera direccin. Y en ese caso, el valor ser mayor segn sea mayor el nmero de bytes que emplee el tipo de dato referenciado por el puntero. Si es un char, entre la posicin ltima y la primera hay, efectivamente, 9 elementos; y el nmero de bytes entre esas dos direcciones tambin es 9. Si es un float, entre la posicin ltima y la primera hay, efectivamente y de nuevo, 9 elementos; pero ahora el nmero de bytes entre esas dos direcciones es 36, porque cada uno de los nueve elementos ocupa cuatro bytes de memoria.

472

Captulo 16. Punteros.

ndices y operatoria de punteros.


Se puede recorrer un vector, o una cadena de caracteres mediante ndices. Y tambin, de forma equivalente, mediante operatoria de punteros. Pero adems, los arrays y cadenas tienen la siguiente propiedad: Si declaramos ese array o cadena de la siguiente forma: tipo nombre_array[dimensin]; El nombre del vector o cadena es nombre_array. Para hacer uso de cada una de las variables, se utiliza el nombre del vector o cadena seguido, entre corchetes, del ndice del elemento al que se quiere hacer referencia: nombre_array[ndice]. Y ahora introducimos otra novedad: el nombre del vector o cadena recoge la direccin de la cadena, es decir, la direccin del primer elemento de la cadena: decir nombre_array es lo mismo que decir &nombre_array[0]. Y por tanto, y volviendo al cdigo anteriormente visto: long a[10], *pa; short b[10], *pb; pa = &a[0]; pb = &b[0]; Tenemos que *(pa + i) es lo mismo que a[i]. Y como decir a es equivalente a decir &a[0] entonces, decir pa = &a[0] es lo mismo que decir pa = a, y trabajar con el valor *(pa + i) es lo mismo que trabajar con el valor *(a + i). Y si podemos considerar que dar el nombre de un vector es equivalente a dar la direccin del primer elemento, entonces podemos considerar que ese nombre funciona como un puntero constante, con quien se pueden hacer operaciones y formar parte de expresiones, mientras no se le coloque en la parte Lvalue de un operador asignacin.

473

Informtica Aplicada. Programacin en Lenguaje C.

Y muchos programadores, en lugar de trabajar con ndices, recorren todos sus vectores y cadenas mediante la operatoria o aritmtica de punteros. Veamos un programa sencillo, resuelto mediante ndices de vectores y mediante la operatoria de punteros. Por ejemplo, un programa que solicite al usuario una cadena de caracteres y luego la copie en otra cadena en orden inverso: primero el ltimo carcter, luego el penltimo, etc. Con ndices: #include <stdio.h> #include <string.h> int main(void) { char orig[100], copia[100]; short i, l; printf("Introduzca la cadena ... \n"); gets(orig); l = strlen(orig); for(i = 0 ; i < l ; i++) copia[l - i - 1] = orig[i]; copia[i] = NULL; printf("Cadena original: %s\n",orig); printf("Cadena copia: %s\n",copia); return 0; } Con operatoria de punteros: #include <stdio.h> #include <string.h> int main(void) { char orig[100], copia[100]; short i, l; printf("Introduzca la cadena ... \n"); gets(orig); l = strlen(orig); for(i = 0 ; i < l ; i++) *(copia + l - i - 1) = *(orig + i);

474

Captulo 16. Punteros.

*(copia + i) = NULL; printf("Cadena original: %s\n",orig); printf("Cadena copia: %s\n",copia); return 0; } Desde luego, ambas formas de referirse a los distintos elementos del vector son vlidas. En el captulo en que hemos presentado los arrays hemos indicado que es competencia del programador no recorrer el vector ms all de las posiciones reservadas. Si se llega, mediante operatoria de ndices o mediante operatoria de punteros a una posicin de memoria que no pertenece realmente al vector, el compilador no detectar error alguno, e incluso puede que tampoco se produzca un error en tiempo de ejecucin, pero estaremos accediendo a zona de memoria que quiz se emplea para almacenar otra informacin. Y entonces alteraremos esos datos de forma inconsiderada, con las consecuencias desastrosas que eso pueda llegar a tener para el buen fin del proceso. Cuando en un programa se llega equivocadamente, mediante operatoria de punteros o de ndices, ms all de la zona de memoria reservada, se dice que se ha producido o se ha incurrido en una violacin de memoria.

Puntero a puntero.
Un puntero es una variable que contiene la direccin de otra variable. Segn sea el tipo de variable que va a ser apuntada, as, de ese tipo, debe ser declarado el puntero. Ya lo hemos dicho. Pero un puntero es tambin una variable. Y como variable que es, ocupa una porcin de memoria: tiene una direccin. Se puede, por tanto, crear una variable que almacene la direccin de esa variable puntero. Sera un puntero que almacenara direcciones de tipo de dato puntero. Un puntero a puntero.

475

Informtica Aplicada. Programacin en Lenguaje C.

Por ejemplo: float F, *pF, **ppF; Acabamos de crear tres variables: una, de tipo float, llamada F. Una segunda variable, de tipo puntero a float, llamada pF. Y una tercera variable, de tipo puntero a puntero float, llamada ppF. Y eso no es un rizo absurdo. Tiene mucha aplicacin en C. Igual que se puede hablar de un puntero a puntero a puntero a puntero a float. Y as como antes hemos visto que hay una relacin directa entre punteros a un tipo de dato y vectores de este tipo de dato, tambin veremos ahora que hay una relacin directa entre punteros a punteros y matrices de dimensin 2. Y entre punteros a punteros a punteros y matrices de dimensin 3. Y si es conveniente trabajar con matrices de dimensin n , entonces tambin lo es trabajar con punteros a punteros a punteros Vemoslo con un ejemplo. Supongamos que creamos una matriz de dimensin 2: double m[4][6]; Antes hemos dicho que al crea un array, al hacer referencia a su nombre estamos indicando la direccin del primero de sus elementos. Ahora, al crear esta matriz, la direccin del elemento m[0][0] la obtenemos con el nombre de la matriz: Es equivalente decir m que decir &m[0][0]. Pero la estructura que se crea al declarar una matriz es algo ms compleja que una lista de posiciones de memoria. En el ejemplo expuesto de la matriz double, se puede considerar que se han creado cuatro vectores de seis elementos cada uno y colocados en la memoria uno detrs del otro de forma consecutiva. Y cada uno de esos vectores tiene, como todo vector, la posibilidad de ofrecer la direccin de su primer elemento. El cuadro 16.1. presenta un esquema de esta construccin. Desde luego, no existen los punteros m, ni ninguno de los

476

Captulo 16. Punteros.

*(m + i). Pero si empleamos el nombre de la matriz de esta forma, entonces trabajamos con sintaxis de punteros. De hecho, si ejecutamos el siguiente programa: #include <stdio.h> int main(void) { double m[4][6]; short i; printf("m = %p\n",m); for(i = 0 ; i < 4 ; i++) { printf("*(m + %hd) = %p\t",i, *(m + i)); printf("&m[%hd][0] = %p\n",i, &m[i][0]); } return 0: } Obtenemos la siguiente salida: m = *(m *(m *(m *(m 0012FECC + 0) = 0012FECC + 1) = 0012FEFC + 2) = 0012FF2C + 3) = 0012FF5C &m[0][0] &m[1][0] &m[2][0] &m[3][0] = = = = 0012FECC 0012FEFC 0012FF2C 0012FF5C

*(m+0) *(m+1) *(m+2) *(m+3)

m[0][0] m[1][0] m[2][0] m[3][0]

m[0][1] m[1][1] m[2][1] m[3][1]

m[0][2] m[1][2] m[2][2] m[3][2]

m[0][3] m[1][3] m[2][3] m[3][3]

m[0][4] m[1][4] m[2][4] m[3][4]

m[0][5] m[1][5] m[2][5] m[3][5]

Cuadro 16.1.: Distribucin de la memoria en la matriz double m[4][6];

Tenemos que m vale lo mismo que *(m + 0); su valor es la direccin del primer elemento de la matriz: m[0][0]. Despus de l, vienen todos los dems, uno detrs de otro: despus de m[0][5] vendr el m[1][0], y esa direccin la podemos obtener con *(m + 1); despus de m[1][5] vendr el m[2][0], y esa direccin la podemos obtener con *(m + 2);

477

Informtica Aplicada. Programacin en Lenguaje C.

despus de m[2][5] vendr el m[3][0], y esa direccin la podemos obtener con *(m + 3); y despus de m[3][5] se termina la cadena de elementos reservados. Es decir, en la memoria del ordenador, no se distingue entre un vector de 24 variables tipo double y una matriz 4 * 6 de variables tipo double. Es el lenguaje el que sabe interpretar, mediante una operatoria de punteros, una estructura matricial donde slo se dispone de una secuencia lineal de elementos. Esos punteros no existen en realidad, y si se imprime la posicin que ocupa m, *m la pantalla nos mostrar el mismo valor que al solicitarle que nos muestre la direccin de m[0][0]. No existen, pero el lenguaje C admite esa sintaxis, y podemos trabajar como si de punteros constantes se trataran. Y as como antes hemos podido trabajar un programa con un array mediante operatoria de punteros, ahora vamos a hacer lo mismo con un programa que emplee matrices. Veamos un programa que calcula el determinante de una matriz de tres por tres. Con ndices: #include <stdio.h> int main(void) { double m[3][3]; double det; short i,j; for(i = 0 ; i < 3 ; i++) for(j = 0 ; j < 3 ; j++) { printf("m[%hd][%hd] = ", i, j); scanf( %lf",&m[i][j]); } det det det det det det = 0; += (m[0][0] += (m[0][1] += (m[0][2] -= (m[0][2] -= (m[0][1] * * * * * m[1][1] m[1][2] m[1][0] m[1][1] m[1][0] * * * * * m[2][2]); m[2][0]); m[2][1]); m[2][0]); m[2][2]);

478

Captulo 16. Punteros.

det -= (m[0][0] * m[1][2] * m[2][1]); printf("El determinante ... \n"); for(i = 0 ; i < 3 ; i++) { printf("\n | "); for(j = 0 ; j < 3 ; j++) printf("%8.2lf",m[i][j]); printf(" | "); } printf("\n\n es ... %lf",det); return 0; } Con operatoria de punteros: #include <stdio.h> int main(void) { double m[3][3], det; short i,j; for(i = 0 ; i < 3 ; i++) for(j = 0 ; j < 3 ; j++) { printf("m[%hd][%hd] = ", i, j); scanf( %lf",*(m + i) + j); } det det det det det det = 0; += *(*(m+0) += *(*(m+0) -= *(*(m+0) -= *(*(m+0) -= *(*(m+0) + + + + + 0) 2) 2) 1) 0) * * * * * *(*(m+1) *(*(m+1) *(*(m+1) *(*(m+1) *(*(m+1) + + + + + 1) 0) 1) 0) 2) * * * * * *(*(m+2) *(*(m+2) *(*(m+2) *(*(m+2) *(*(m+2) + + + + + 2); 1); 0); 2); 1);

printf("El determinante ... \n"); for(i = 0 ; i < 3 ; i++) { printf("\n | "); for(j = 0 ; j < 3 ; j++) printf("%8.2lf",*(*(m + i) + j)); printf(" | "); } printf("\n\n es ... %lf", det); return 0; }

479

Informtica Aplicada. Programacin en Lenguaje C.

Desde luego, la operatoria de punteros con matrices resulta algo farragosa en un primer momento. Pero no encierra dificultad de concepto.

Modificador de tipo const.


Suponga la siguiente expresin; const double numeroPI = 3.14159265358979323846; const double numeroE = 2.7182818284590452354; Ambas declaraciones comienzan con la palabra clave const. Esta palabra es un modificador de tipo, y as ambas variables creadas (numeroPI y numeroE) no tan solo son de tipo double, sino que adems verifican que son constantes. A partir de esta lnea de declaracin, el compilador vigilar que ninguna lnea de cdigo del programa altere el valor de esas variables: si alguna instruccin pretende variar ese valor, el compilador generara un mensaje de error y el ejecutable no ser finalmente creado. Por eso, una variable declarada como const no podr estar en la parte izquierda del operador asignacin: no podr ser LValue. Como se ha visto en estas dos declaraciones, cuando se crea una variable que deba tener un comportamiento constante, es necesario que en la misma instruccin de la declaracin se indique su valor. Es el nico momento en que una variable const puede formar parte de la LValue de una asignacin: el momento de su declaracin. Ya vimos este modificador de tipo en el Captulo 7, en el epgrafe titulado Constantes (variables const). Directiva #define. Volvemos a este modificador ahora porque juega un papel singular en el comportamiento de los punteros.

480

Captulo 16. Punteros.

Punteros constantes, punteros a constantes y punteros constantes a constantes.


Trabajando con punteros y con el modificador de tipo const, podemos distinguir tres situaciones diferentes: PUNTERO A CONSTANTE: Es un puntero cuyo valor es la direccin de una variable (apunta a una variable), no necesariamente const, pero que el puntero considerar constante. En este caso, no se podr modificar el valor de la variable apuntada por el puntero; pero s se podr variar el valor del puntero: es decir, el puntero podr apuntar a otra variable. Por ejemplo: double x = 5, y = 2; const double *ptrx = &x; double const *ptry = &y; Ambas declaraciones de las variables punteros son equivalentes: ambas declaran un puntero que tratar a la variable apuntada como constante. Estar por tanto permitido cambiar el valor de las variables x y (de hecho no se han declarado como constantes); estar permitido cambiar el valor de los punteros ptrx y ptry (de hecho esos dos punteros no son constantes); pero no podremos modificar los valores de las variables x e y mediante la indireccin desde los punteros. x *= 2; y /= 2; ptrx = &y; ptry = &x; *ptrx *= 2; *ptry /= 2; Es importante // // // // // // sealar operacin operacin operacin operacin operacin operacin que las permitida. permitida. permitida. permitida. PROHIBIDA. PROHIBIDA. apuntadas no son

variables

necesariamente de tipo const. Pero los punteros ptrx y ptry han sido

481

Informtica Aplicada. Programacin en Lenguaje C.

declarados de tal manera que cada uno ellos puede acceder al valor de las variable a la que apunta, pero no puede alterar ese valor. El uso de este tipo de puntero es muy frecuente en los parmetros de las funciones, y ya los veremos en el Captulo 17 del manual. PUNTERO CONSTANTE: Es un puntero constante (const) cuyo valor es la direccin de una variable (apunta a una variable) que no es necesariamente constante. En este caso, el valor del puntero no podr variarse y, por tanto, el puntero apuntar siempre a la misma variable; pero s se podr variar el contenido de la variable apuntada. La forma de declarar un puntero contante es la siguiente: double x = 5, y = 2; double* const ptrx = &x; double* const ptry = &y; Con estas nuevas declaraciones, ahora estas son las sentencias permitidas o prohibidas: x *= 2; y /= 2; ptrx = &y; ptry = &x; *ptrx *= 2; *ptry /= 2; // // // // // // operacin operacin operacin operacin operacin operacin permitida. permitida. PROHIBIDA. PROHIBIDA. permitida. permitida.

Con un puntero constante est permitido acceder, mediante el operador indireccin, al valor de la variable apuntada, pero no se puede cambiar el valor del puntero: no se puede asignar al puntero la direccin de otra variable. Es interesante comparar la forma de las declaraciones de los dos tipos de puntero presentados hasta el momento: double* const ptrx = &x; const double* ptry = &y; El puntero ptrx es un puntero constante: no podemos cambiar su valor: siempre apuntar a la variable x; el puntero ptry es un puntero a constante: s podemos cambiar el valor del puntero: es

482

Captulo 16. Punteros.

decir, podr apuntar a distintas variables; pero en ningn caso podremos cambiar el valor de esas variables a las que apunta: y eso aunque esas variables no se hayan declarado con el modificador const.: no es que las variables sean constantes (aunque, desde luego, podran serlo): es que el puntero no puede hacer modificacin sobre ellas. PUNTERO CONSTANTE A CONSTANTE: Es un puntero constante (const) cuyo valor es la direccin de una variable (apunta a una variable) sobre la que no podr hacer modificacin de su valor. Es una mezcla o combinacin de las dos definiciones anteriores. El modo en que se declara un puntero constante a constante es el siguiente: double x = 5; const double * const ptrx = &x El puntero ptrx es constante, por lo que no podremos cambiar su valor: apunta a la variable x, y no podemos asignarle cualquier otra direccin de cualquier otra variable. Y adems es un puntero a constante, por lo que no podremos tampoco cambiar, mediante indireccin, el valor de la variable x. Vea la siguiente lista de declaraciones y sentencias, algunas de las cuales son correctas y otras no, ya que violan alguna de las condiciones establecidas por la palabra clave const. double x = 5, y = 2, z = 7; const double xx = 25; // ptrx: puntero a constante. const double *ptrx = &x; // ptry: puntero constante. double* const ptry = &y; // ptrz: puntero constante a constante. const double* const ptrz = &z; x = 1; // PERMITIDO: x no es const. *ptrx = 1; // PROHIBIDO: ptrx es un puntero a constante. ptrx = &xx; // PERMITIDO: ptrx no es un puntero constante. xx = 30; // PROHIBIDO: xx es const.

483

Informtica Aplicada. Programacin en Lenguaje C.

y = 1; *ptry = 1; ptry = &x; z = 1; ptrz = &x; *ptrz = 1;

// PERMITIDO: y no es const. // PERMITIDO: ptry no es un puntero a constante. // PROHIBIDO: ptry es un puntero constante. // PERMITIDO: z no es const. // PROHIBIDO: ptrz es un puntero constante. // PROHIBIDO: ptrz es un puntero a constante.

Es importante utilizar adecuadamente este modificador (const) en la declaracin y uso de los punteros. Veamos un ejemplo sencillo, donde se produce una alteracin, a travs de un puntero, del valor de una variable declarada como const. int main(void) { const double x = 3; double *ptrx = &x; *ptrx = 5; printf("El valor de x es ... %.2lf\n", x); return 0; } La variable x es constante: no se debera poder modificar su valor, una vez inicializada al valor 3. Pero no hemos declarado el puntero ptrx como const. Y, por tanto, desde l se puede acceder a la posicin de memoria de la variable x y proceder a su cambio de valor. La prctica correcta habitual ser que una variable declarada como const nicamente pueda ser apuntada por un puntero que preserve esa condicin. Pero eso no es algo que vigile el compilador, y el cdigo arriba copiado compila correctamente (segn cmo tenga configurado el compilador quiz pueda obtener un mensaje de advertencia o warning) y muestra por pantalla en mensaje El valor de x es ... 5.00

484

Captulo 16. Punteros.

Punteros fuera del mbito de la variable a la que apuntan.


Con un puntero podemos acceder a la informacin de variables que estn fuera del mbito actual. E incluso podemos variar su valor. De hecho esta capacidad de alcanzar al valor de una variable desde fuera de su mbito es un uso muy frecuente para los punteros en las llamadas a funciones, como veremos en el Captulo 17. Veamos ahora un ejemplo sencillo, dentro de la propia funcin main. Primero presentamos el siguiente cdigo, donde se emplea una variable static: una variable que se extiende a la duracin de todo el programa, pero cuyo mbito queda reducido al del bloque en el que se ha definido. Esa variable, que en el ejemplo llamamos local, se inicializa a cero la primera vez que se entra en el bloque donde est definida, y posteriormente se va aumentando de uno en uno cada vez que se ejecuta su bloque. #include <stdio.h> int main(void) { short a, b = 0; do { a = 0; do { static short local = 0; local++; printf("local = %2hd\n",local); a++; }while (a < 5); printf("\n"); b++; }while(b < 5); return 0; } La salida de este programa es la que sigue: local = local = 1 6 local = local = 2 7 local = local = 3 8 local = local = 4 9 local = 5 local = 10

485

Informtica Aplicada. Programacin en Lenguaje C.

local = 11 local = 16 local = 21

local = 12 local = 17 local = 22

local = 13 local = 18 local = 23

local = 14 local = 19 local = 24

local = 15 local = 20 local = 25

Cada vez que se entra en el mbito de la variable local, se vuelve a poder trabajar sobre ella y se sigue incrementando desde el valor en que quedo despus de la ltima modificacin. Pero si ahora introducimos en el programa los siguientes cambios: #include <stdio.h> int main(void) { short a, b = 0; short *ptr_a_local; do { a = 0; do { static short local = 0; ptr_a_local = &local; local++; printf("local = %hd\t",local); a++; }while (a < 5); printf("\n"); b++; *ptr_a_local = 0; }while(b < 5); return 0; } Entonces la salida es: local local local local local = = = = = 1 1 1 1 1 local local local local local = = = = = 2 2 2 2 2 local local local local local = = = = = 3 3 3 3 3 local local local local local = = = = = 4 4 4 4 4 local local local local local = = = = = 5 5 5 5 5

Y es que con el puntero ptr_a_local, y desde fuera del mbito de la variable local, le hemos cambiado su valor y la hemos inicializado a cero una y otra vez.

486

Captulo 16. Punteros.

Ejercicios.

16.1.

Leer el siguiente cdigo y completar la salida que ofrece por pantalla: (No est resuelto).

#include <stdio.h> void main(void) { char c[3]; short i[3], cont; float f[3]; printf("Las direcciones for(cont = 0 ; cont < 3 { printf("&c[%2d] = printf("&i[%2d] = printf("&f[%2d] = } }

de memoria son:\n"); ; cont++) %10p\t",cont,c + cont); %10p\t",cont,i + cont); %10p\n\n",cont,f + cont);

Las direcciones de memoria son:


&c[ 0] = 0064FE01 &c[ 1] = &c[ 2] = &i[ 0] = 0064FDF8 &i[ 1] = &i[ 2] = &f[ 0] = 0064FDEC &f[ 1] = &f[ 2] =

16.2.

Leer el siguiente cdigo y completar la salida que ofrece por pantalla.

#include <stdio.h> main() { char c[20],*pc1,*pc2; short int i[20],*pi1,*pi2; float f[20],*pf1,*pf2; pc1 = &c[0];

487

Informtica Aplicada. Programacin en Lenguaje C.

pc2 pi1 pi2 pf1 pf2

= = = = =

&c[19]; &i[0]; &i[19]; &f[0]; &f[19];

printf("(int)pc2-(int)pc1 es %d\n",(int)pc2-(int)pc1); printf("pc2 - pc1 es %d\n\n",pc2 - pc1); printf("(int)pi2-(int)pi1 es %d\n",(int)pi2-(int)pi1); printf("pi2 - pi1 es %d\n\n",pi2 - pi1); printf("(int)pf2-(int)pf1 es %d\n",(int)pf2-(int)pf1); printf("pf2 - pf1 es %d\n\n",pf2 - pf1); }

(int)pc2 - (int)pc1 es 19 pc2 - pc1 es 19 (int)pi2 - (int)pi1 es 38 pi2 - pi1 es 19 (int)pf2 - (int)pf1 es 76 pf2 - pf1 es 19

Estos dos ejercicios planteados son completamente tericos. No me imagino una situacin concreta en la que se necesite una resta de punteros. Son simplemente formulaciones que pueden ayudar a afianzar los conceptos de este Captulo. En general una buena forma de aprender a manejar punteros es intentar rehacer todos los ejercicios ya resueltos en los captulos de vectores y de cadenas de texto (Captulos 10 y 11) empleando ahora operatoria de punteros y recorriendo los vectores y matrices mediante la indireccin. Le recomiendo que as lo haga, al menos con algunos de cada tema, tantos con ejercicios de vectores y cadenas de texto, como con ejercicios de matrices. No los presento resueltos de nuevo porque la algoritmia es la misma, y lo nico que vara es el modo en que se expresan los elementos de los arrays.

488

Captulo 16. Punteros.

Recuerde: Decir a[i], es equivalente a decir *(a + i) Decir &a[i] es equivalente a decir a + i. Decir a[i][j] es equivalente a decir *(*a + i) + j) Y decir &a[i][j] es equivalente a decir (*a + i) + j).

No hay ms posibilidades. Ahora practique.

489

Informtica Aplicada. Programacin en Lenguaje C.

490

CAPTULO 17
FUNCIONES Y PARMETROS CON PUNTEROS.
Ya hemos visto un primer captulo que versaba sobre las funciones. Luego hemos presentado otro captulo que presentaba una forma especial de definir funciones: aquellas que se definan por recursividad: aquellas que recurran a s mismas para establecer su propia definicin. Pero en ambos casos hemos trabajado con parmetros sencillos: variables declaradas en el mbito de la funcin que invocaba a la nuestra implementada. Pero a veces es necesario utilizar punteros en los parmetros de las funciones. Qu ocurre si deseamos que una funcin realice cambios en los valores de varias variables definidas en el mbito de la funcin que invoca? O cmo procedemos si lo que deseamos pasar como parmetro a una funcin no es una variable sino una matriz, de por ejemplo, 10 por 10 elementos?

Informtica Aplicada. Programacin en Lenguaje C.

Llamadas por valor y llamadas por referencia.


Introducimos aqu dos nuevos conceptos, tradicionales al hablar de funciones. Y muy importantes. Hacen referencia al modo en que la funcin recibe los parmetros. Hasta ahora, en todos los ejemplos previos presentados en los Captulos 13 y 14, hemos trabajado haciendo llamadas por valor. Decimos que una funcin es llamada por valor cuando se copia el valor del argumento en el parmetro formal de la funcin. Una variable est en la funcin que llama; y otra variable, distinta, es la que recibe el valor en la funcin llamada. La funcin llamada no puede alterar el valor del argumento original de la funcin que llama. nicamente puede cambiar el valor de su variable local que ha recibido por asignacin el valor de esa variable en el momento en que se realiz la llamada a la funcin. As, en la funcin llamada, cada argumento es efectivamente una variable local inicializada con el valor con que se llam a la funcin. Pero supongamos, por ejemplo, que necesitamos en nuestro programa realizar con mucha frecuencia la tarea de intercambiar el valor de dos variables. Ya sabemos cmo se hace, y lo hemos visto resuelto tanto a travs de una variable auxiliar como gracias al operador or exclusivo. Sera muy conveniente disponer de una funcin a la que se le pudieran pasar, una y otra vez, el par de variables de las que deseamos intercambiar sus valores. Pero cmo lograr hacer ese intercambio a travs de una funcin si todo lo que se realiza en la funcin llamada muere cuando termina su ejecucin? Cmo lograr que en la funcin que invoca ocurra realmente el intercambio de valores entre esas dos variables? La respuesta no es trivial: cuando invocamos a la funcin (que llamaremos en nuestro ejemplo intercambio), las variables que deseamos intercambiar dejan de estar en su mbito y no llegamos a

492

Captulo 17. Funciones y parmetros con punteros.

ellas. Toda operacin en memoria que realice la funcin intercambio morir con su ltima sentencia: su nico rastro ser, si acaso, la obtencin de un resultado, el que logra sobrevivir de la funcin gracias a la sentencia return. Y aqu llegamos a la necesidad de establecer otro tipo de llamadas a funciones: las llamadas por referencia. En este tipo de llamada, lo que se transfiere a la funcin no es el valor del argumento, sino la direccin de memoria de la variable argumento. Se copia la direccin del argumento en el parmetro formal, y no su valor. Evidentemente, en ese caso, el parmetro formal deber ser de tipo puntero. En ese momento, la variable argumento quedar fuera de mbito, pero a travs del puntero correspondiente en los parmetros formales podr llegar a ella, y modificar su valor. La funcin intercambio podra tener el siguiente prototipo: void intercambio(long*,long*); Y su definicin podra ser la siguiente: void intercambio(long*a,long*b) { *a ^= *b; *b ^= *a; *a ^= *b; } O tambin void intercambio(long*a,long*b) { short aux; aux = *b; *b = *a; *a = aux; } Supongamos que la funcin que llama a la funcin intercambio lo hace de la siguiente forma: intercambio(&x,&y);

493

Informtica Aplicada. Programacin en Lenguaje C.

Donde lo que se le pasa a la funcin intercambio son las direcciones (no los valores) de las dos variables de las que se desea intercambiar sus valores. En la funcin llamante tenemos: <x, short, , > y <y, short, , >

En la funcin intercambio tenemos: <a, short*, , > y <b, short*, , >

Es decir, dos variables puntero cuyos valores que se le van asignar sern las posiciones de memoria de cada una de las dos variables usadas como argumento, y que son con las que se desea realizar el intercambio de valores. La funcin trabaja sobre los contenidos de las posiciones de memoria apuntadas por los dos punteros. Y cuando termina la ejecucin de la funcin, efectivamente, mueren las dos variables puntero a y b creadas. Pero ya han dejado hecha la faena en las direcciones que recibieron al ser creadas: en codificado el valor ahora queda codificado el valor ; y en queda . Y en cuanto termina la ejecucin de intercambio

regresamos al mbito de esas dos variables x e y: y nos las encontramos con los valores intercambiados. Muchos son los ejemplos de funciones que, al ser invocadas, reciben los parmetros por referencia. La funcin scanf recibe el parmetro de la variable sobre la que el usuario deber indicar su valor con una llamada por referencia. Tambin lo hemos visto en la funcin gets, que recibe como parmetro la direccin de la cadena de caracteres donde se almacenar la cadena que introduzca el usuario. Por otro lado, siempre que deseemos que una funcin nos devuelva ms de un valor tambin ser necesario utilizar llamadas por referencia: uno de los valores deseamos podremos recibirlo gracias a la sentencia return de la funcin llamada; los dems podrn quedar en los

494

Captulo 17. Funciones y parmetros con punteros.

argumentos pasados como referencia: entregamos a la funcin sus direcciones, y ella, al terminar, deja en esas posiciones de memoria los resultados deseados.

Vectores y matrices como argumentos.


Y si podemos pasar la direccin de una variable, entonces tambin podemos pasar la direccin de un array, o de una matriz, o de una cadena de caracteres. As, cuando queremos pasar como argumento un vector, no es necesario hacer copia de todo l en la lista de parmetros: basta pasar como parmetro la direccin del primer elemento del vector. La funcin podr acceder a todos los elementos del vector mediante operatoria de punteros o mediante ndices. Habitualmente, al pasar un array o matriz, ser necesario pasar, como otros parmetros, la dimensin o dimensiones de ese array o matriz. La llamada de la funcin usar el nombre del vector como argumento, ya que, como dijimos al presentar los arrays y las cadenas, el nombre de un array o cadena, en C, indica su direccin: decir nombre_vector es lo mismo que decir &nombre_vector[0]. Evidentemente, y como siempre, el tipo de dato puntero del parmetro formal debe ser compatible con el tipo de dato del vector argumento. Existen tres formas de declarar un parmetro formal que va a recibir un puntero a un vector: tipo nombre(tipo vector[dimensin]); es decir, declarando el

parmetro como un vector dimensionado. tipo nombre(tipo vector[]); es decir, declarando el parmetro como un vector sin tamao determinado.

495

Informtica Aplicada. Programacin en Lenguaje C.

tipo nombre(tipo*); es decir, declarando el parmetro como un puntero. Veamos un ejemplo. Hagamos una aplicacin que reciba un array de variables tipo float y nos indique cul es el menor de sus valores y cul el mayor. Entre los parmetros de la funcin ser necesario indicar tambin la dimensin del vector. Lo primero que hemos de pensar es cmo pensamos devolver, a la funcin que llame a nuestra funcin, los dos valores solicitados. Repito: DOS valores solicitados. No podremos hacerlo mediante un return, porque as slo podramos facilitar uno de los dos. Por eso, entre los parmetros de la funcin tambin necesitamos dos que sean las direcciones donde deberemos dejar recogidos el mayor de los valores y el menor de ellos. Entonces la funcin podr tener el siguiente prototipo: void extremos(float*v, unsinged short d, float*M, float*m); El primer parmetro es la direccin del array donde se recogen todos los valores. En segundo parmetro la dimensin del array. El tercero y el cuarto las direcciones donde se consignarn los valores mayor (variable M) y menor (variable m) del array. Como todos los valores que la funcin determina ya quedan reservados en las variables que se le pasan como puntero, sta no necesita devolver ningn valor, y se declara, por tanto, de tipo void. El cdigo de la funcin podra ser el siguiente: void extremos(float*v, unsigned short d, float*M, float*m) { short int i; *M = *v; *m = *v; for(i = 0 ; i < d ; i++) { if(*M < *(v + i)) *M = *(v + i); if(*m > *(v + i)) *m = *(v + i); } }

496

Captulo 17. Funciones y parmetros con punteros.

Inicialmente hemos puesto como menor y como mayor el primero de los elementos del vector. Y luego lo hemos recorrido, y siempre que hemos encontrado un valor mayor que el que tenamos consignado como mayor, hemos cambiado y hemos guardado ese nuevo valor como el mayor; y lo mismo hemos hecho con el menor. Y al terminar de recorrer el vector, ya han quedado esos dos valores guardados en las direcciones de memoria que hemos recibido como parmetros. Para llamar a esta funcin bastar la siguiente sentencia: extremos(vector, dimension, &mayor, &menor); La recepcin de un vector como parmetro formal no necesariamente debe hacerse desde el primer elemento del vector. Supongamos que al implementar la funcin extremos exigimos, como especificacin tcnica de esa funcin, que la matriz tenga una dimensin impar. Y diremos que reciba como parmetros los mismos que antes. Pero ahora la direccin de memoria del vector ser la del elemento que est a la mitad del vector. Si la dimensin es , el usuario de la funcin deber pasar como argumento la direccin del elemento de ndice indicado . . Y el argumento segundo deber ser, en lugar de la dimensin del vector, el valor

El prototipo de la funcin es exactamente el mismo que antes: void extremos2(float*v, unsinged short d, float*M, float*m); Y su definicin podra ser la siguiente: void extremos2(float*v, { short int i; *M = *v; *m = *v; for(i = 0 ; i < d { if(*M < *(v if(*M < *(v if(*m > *(v if(*M < *(v } } unsigned short d, float*M, float*m)

; i++) + + i)) i)) i)) i)) *M *M *m *M = = = = *(v *(v *(v *(v + + i); i); i); i);

497

Informtica Aplicada. Programacin en Lenguaje C.

Donde as logramos la misma operacin de bsqueda y hemos reducido a la mitad los incrementos de la variable contador i. No entramos ahora en analizar la oportunidad de esta nueva versin de la funcin. Queremos sealar simplemente que el cdigo puede hacer lo que a nosotros nos convenga ms. Lo importante en este caso es dejar bien especificadas las condiciones para el correcto uso de la funcin. Y vigilar cules son los lmites del vector y no permitir que se acceda a posiciones de memoria que estn fuera de la dimensin del vector.

Argumentos tipo puntero constante.


Ya hemos explicado en el Captulo 16 cmo los punteros se pueden definir como constantes (no se puede cambiar su valor) o como a constantes (no se puede variar el valor de la variable apuntada). Con frecuencia una funcin debe recibir, como parmetro, la direccin de un array. Y tambin frecuentemente ocurre que esa funcin no ha de realizar modificacin alguna sobre los contenidos de las posiciones del array sino simplemente leer los valores. En ese supuesto, es prctica habitual declarar el parmetro del array como de tipo const. As se ofrece una garanta al usuario de la funcin de que realmente no se producir modificacin alguna a los valores del array que pasa como parmetro. Eso se puede ver en muchos prototipos de funciones ya presentadas: por ejemplo, las vistas en el Captulo 12. Veamos algunos ejemplos de prototipos de funciones del archivo de cabecera string.h: char *strcpy(char *s1, const char *s2); int strcmp(const char *s1, const char *s2); char *strcat(char*s1, const char *s2); El prototipo de la funcin strcpy garantiza al usuario que la cadena recibida como segundo parmetro no sufrir alteracin alguna; no as la

498

Captulo 17. Funciones y parmetros con punteros.

primera, que deber ser, finalmente, igual a la cadena recibida como segundo parmetro. Si la funcin strcpy intentase cambiar en algo alguno de los valores del array recibido como segundo parmetro, la aplicacin no se podra compilar. Lo mismo podramos decir de la funcin strcmp, que lo que hace es comparar las dos cadenas recibidas. En ese caso, la funcin devuelve un valor entero menor que cero si la primera cadena es menor que la segunda, o mayor que cero si la segunda es menor que la primera, o igual a cero si ambas cadenas son iguales. Y lo que garantiza el prototipo de esta funcin es que en todo el proceso de anlisis de ambas cadenas, ninguna de las dos sufrir alteracin alguna: de lo contrario, la aplicacin no compilara. Y lo mismo podremos decir de la funcin strcat. El primer parmetro sufrir alteracin pues al final de la cadena de texto contenida se le adjuntar la cadena recibida en el segundo parmetro. Lo que queda garantizado en el prototipo de la funcin es que el segundo parmetro no sufrir alteracin alguna. Muchas de las funciones que hemos presentado en las pginas previas en el presente captulo bien podran haber declarado alguno de sus parmetros como const. Hubiera sido mejor prctica de programacin.

Recapitulacin.
Completando la visin del uso de funciones ya iniciada en los captulos 13 y 14, hemos presentado ahora el modo en que a una funcin se le puede pasar, como parmetro, no un valor concreto de una variable, sino su propia direccin, permitiendo as que la funcin invocada pueda, de hecho, modificar los valores de las variables definidas en el mbito de la funcin que invoca. Tambin hemos aprendido cmo una funcin puede recibir todo un array o una matriz de valores.

499

Informtica Aplicada. Programacin en Lenguaje C.

Ejercicios.

17.1.

Escriba una funcin que reciba como parmetros un vector de enteros y un entero que recoja la dimensin del vector, y devuelva ese vector, con los valores enteros ordenados de menor a mayor. Ya conocemos el algoritmo de ordenacin de la burbuja. Lo hemos utilizado anteriormente. Un posible programa que utilice esta funcin que presentamos podra ser el siguiente: #include <stdio.h> #include <stdlib.h> #define TAM 100 // Declaracin de las funciones ... void asignar(long *, short); void ordenar(long *, short); void mostrar(const long *, short); // Funcin principal ... int main(void) { long vector[TAM]; asignar(vector, mostrar(vector, ordenar(vector, mostrar(vector, return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ /* Funcin de asignacin. -------------------------------- */ void asignar(long *v, short d) TAM); TAM); TAM); TAM); // // // // Asignar valores. Antes de ordenar. Se ordena el vector. Despes de ordenar.

500

Captulo 17. Funciones y parmetros con punteros.

{ short i; for(i = 0 ; i < d ; i++) { printf("Valor %hd ... , i + 1); scanf( %ld", v + i); } } /* Funcin de ordenacin. -------------------------------- */ void ordenar(long *v , short d) { short i, j; for(i = 0 ; i < d ; i++) for(j = i + 1 ; j < d ; j++) if(*(v + i) > *(v + j)) { *(v + i) ^= *(v + j); *(v + j) ^= *(v + i); *(v + i) ^= *(v + j); } } /* Funcin que muestra el vector que recibe como parmetro.*/ void mostrar(const long *v , short d) { printf("\n\n"); short i; for(i = 0 ; i < d ; i++) printf("%5ld", *(v + i)); }

Hemos definido tres funciones: una muestra las valores de un vector que recibe como primer parmetro, cuya dimensin queda indicada por el segundo parmetro. Otra ordena los valores de ese array. La tercera, simplemente, le asigna valores mediante entrada de teclado. Las tres funciones reciben como parmetros el valor de la dimensin, que es un valor tomado de una directiva define, perfectamente accesible en cualquier punto del programa. Y es que aunque el valor de la macro TAM es accesible desde ambas funciones, stas, como es exigido en un correcto diseo de funciones, no dependen para nada del entorno en el que estn definidas. Tal y como estn implementadas en nuestro

501

Informtica Aplicada. Programacin en Lenguaje C.

ejercicio pueden ser exportadas a cualquier sitio, pues para nada depende su correcta ejecucin de ningn valor o parmetro, o macro, definido en el entorno en el que se han definido aqu y ahora ambas funciones. En todo momento se ha utilizado en las funciones la aritmtica de punteros y el operador indireccin. Evidentemente se podra hacer lo mismo con los ndices del vector. De hecho la expresin *(v + i) es siempre intercambiable por la expresin v[i]. Como se ve, en el prototipo de la funcin mostrar hemos declarado el puntero const long *v, como constante, porque la funcin no modifica en ningn momento ningn valor del array recibido por el puntero v.

17.2.

Escriba una aplicacin que reciba un entero y busque todos sus divisores, dejndolos en un vector. La aplicacin debe tener una funcin que se encargar de buscar los divisores. Esta funcin recibir como parmetros el entero sobre el que hay que buscar sus divisores, el vector donde debe dejar los enteros, y la dimensin del vector. La funcin devuelve un valor positivo igual al nmero de divisores hallados si el proceso termina correctamente. Deber devolver un valor negativo si falla en el proceso de bsqueda de los divisores.

De nuevo conocemos el algoritmo. Hemos resuelto este ejercicio en captulos anteriores. Pero ahora lo hacemos utilizando funciones. La funcin main quedar para organizar el trabajo, pero no para llevarlo directamente a cabo. He aqu una posible solucin a la necesidad de programa planteada:

502

Captulo 17. Funciones y parmetros con punteros.

#include <stdio.h> #define TAM 100 // Declaracin de las funciones ... short divisores(long, long*, short); void mostrar(const long *, short); // Funcin principal ... int main(void) { long N, vector[TAM]; short div; printf("Introduzca un entero ... "); scanf( %ld",&N); if((div = divisores(N,vector, TAM)) > 0) mostrar(vector, div); else printf("No se ha podido realizar la operacin"); return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ /* Funcin de bsqueda de divisores. --------------------- */ short divisores(long N, long *v , short d) { short cont = 1; short div; v[0] = 1; for(div = 2 ; div <= N / 2 ; div++) if(N % div == 0) { v[cont] = div; cont++; // Si no caben ms divisores, se aborta la operacin. if(cont >= d) return -1; } v[cont] = N; return cont + 1; }

503

Informtica Aplicada. Programacin en Lenguaje C.

/* Funcin que muestra el vector que recibe como parmetro.*/ void mostrar(const long *v , short d) { short i; printf("\n\n"); for(i = 0 ; i < d ; i++) printf("%5ld", *(v + i)); }

17.3.

Escriba una funcin, y un programa que le use, que calcule el mximo comn divisor de un conjunto de enteros que recibe en un vector como parmetro Adems, la funcin recibe otro parmetro que es el nmero de enteros recogidos en ese vector.

Conocemos el algoritmo de Euclides. sa es toda la algoritmia que requiere la resolucin de nuestro problema. Una posible implementacin podra ser la siguiente: #include <stdio.h> #define TAM 100 // Declaracin de funciones ... long mcd(long, long); long MCD(long*,short); // Funcin principal ... int main(void) { long numeros[100]; long m; short i; // Introduccin de valores ... printf("Introduzca valores."); printf("Al terminar introduzca en valor cero."); for(i = 0 ; i < TAM ; i++) { printf("\n\nnumeros[%3hu] -> ",i); scanf( %lu",numeros + i); if(*(numeros + i) == 0) break;

504

Captulo 17. Funciones y parmetros con punteros.

} // Clculo del mximo comn divisor ... m = MCD(numeros,i); printf("El maximo comun divisor es ... %lu",m); return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ // mcd de dos enteros. Definicin por recurrencia. long mcd(long a, long b) { return b ? mcd(b,a % b) : a; } // Funcin clculo mcd de varios enteros ... long MCD(long*n,short d) { short i; long m; // Si algn entero es 0, el mcd lo ponemos a cero. for(i = 0 ; i < d ; i++) if(!*(n + i)) return 0; // Si solo hay un entero, el mcd es ese entero. if(d == 1) return *(n + 0); i = 2; m = mcd(*(n + 0),*(n + 1)); while(i < d) m = mcd(m,*(n + i++)); return m; }

17.4.

Necesitamos una funcin que reciba como parmetro un array de enteros largos. Tantos enteros como indique el segundo parmetro de la funcin. Esta funcin deber devolver el valor +1 si todos los valores del array son diferentes, y el valor 0 si hay alguna repeticin.

505

Informtica Aplicada. Programacin en Lenguaje C.

El prototipo de la funcin es: short test_valores(const long*, long); Devuelve un valor short igual a cero o a uno como ya hemos explicado; y recibe como parmetros la direccin del array y la dimensin del array. Defina la funcin.

Algo parecido hemos ya resuelto en el Captulo 10 cuando trabajbamos con arrays de enteros. Simplemente ahora definimos la funcin que realizar esta operacin sugerida en el enunciado: short test_valores(const long*a, long d) { long i, j; for(i = 0 ; i < d ; i++) for(j = i + 1 ; j < d ; j++) if(*(a + i) == *(a + j)) return 0; return 1; }

17.5.

Indique la salida que, por pantalla, ofrece el siguiente cdigo.

#include <stdio.h> #define DIM 5 void f(short*); void m(short*); int main(void) { short a[DIM]; f(a); m(a); return 0; } void f(short* a) {

506

Captulo 17. Funciones y parmetros con punteros.

short i; for(*a = 1 , i = 1 ; i < DIM ; i++) *(a + i) = 2 * *(a + i - 1); } void m(short*a) { short i; for(i = 0 ; i < DIM ; i++) printf("%hd\t", *(a + i)); } La funcin f() asigna valores a un array que recibe como parmetro. La funcin m() simplemente imprime esos valores. Y los valores que asigna la funcin f() al array de enteros cortos son: un 1 para el elemento 0; y el doble que el anterior para todos los dems: el doble de 1 para el elemento 1 (es decir: 2); el doble de 2 para el elemento 2 (es decir: 4); el doble de 4 para el elemento 3 (es decir: 8); y finalmente el doble de 8 para el elemento 4 (es decir: 16). As pues, los valores que mostrar la funcin m() al ser ejecutada, sern: 1 2 4 8 16

17.6.

Escriba una funcin cuyo prototipo es short isNumero(const char*, short); que recibe una cadena de texto como primer parmetro de entrada, y la dimensin de esta variable cadena como segundo parmetro, y devuelve un valor verdadero (distinto de cero) si la cadena contiene nicamente caracteres numricos, y devuelve un valor falso (igual a cero) si la cadena contiene algn carcter que no sea numrico.

507

Informtica Aplicada. Programacin en Lenguaje C.

Cuestin sencilla, que se puede resolver con una funcin del estilo de la que a continuacin se propone: #include <ctype.h> short isNumero(const char* c, short d) { int i; for(i = 0 ; i < d && c[i] != NULL ; i++) if(!isdigit(c[i])) return 0; return 1; } Si encuentra algn carcter no dgito se interrumpe la ejecucin de la iteracin gobernada por la estructura for y se devuelve el valor 0. Si se logra finalizar la ejecucin de todas las iteraciones (se llega al carcter nulo) y no se ha ejecutado esa salida, entonces es que todos los caracteres, hasta llegar a se carcter nulo, son dgitos, y entonces la funcin devuelve un 1.

17.7.

Escriba una funcin cuyo prototipo es long numero(const char*, short); que recibe como primer parmetro de entrada una cadena de texto y como segundo parmetro la dimensin de esa variable cadena, y devuelve (si esa cadena est formada slo por dgitos) el valor numrico de la cifra codificada; en caso contrario (algn carcter no es dgito) devuelve el valor -1. Por ejemplo, si recibe la cadena 2340 debe devolver el valor 2340; si la cadena de entrada es 234r debe devolver el valor -1. Nota: Se recomienda utilizar la funcin definida en la cuestin anterior (16.6), que hemos llamado isNumero. Tambin se puede utilizar la funcin definida en la pregunta 13.8.: la funcin llamada digito.

508

Captulo 17. Funciones y parmetros con punteros.

Una vez definidas las dos funciones sugeridas en el enunciado, sta nueva funcin resulta ms sencilla de plantear y de resolver. Un posible cdigo podra ser el siguiente: long numero(const char* c, short d) { if(!isNumero(c, d)) return -1; long n = 0; for(int i = 0 ; i < d && c[i] ; i++) n = 10 * n + digito(c[i]); return n; } Si no son dgitos todos los elementos, directamente devuelve el valor -1. Si son todo dgitos, entonces calculamos el valor numrico que codifica esa cadena gracias a la funcin digito, que ofrece el valor numrico de cada uno de los dgitos. Habr que multiplicar por 10 cada vez que se incorpore un nuevo dgito en la cifra codificada: ya se resolvi tambin (cfr. ejercicio 12.10) este algoritmo. Una observacin sobre el modificador const del puntero c que se recibe como primer parmetro de la funcin numero. En la funcin se encuentra la llamada a una segunda funcin: a la funcin isNumero. Esta segunda funcin tiene como primer parmetro el mismo puntero c que ha sido previamente declarado constante. El compilador permite que se realice esta llamada porque el mismo prototipo de la funcin isNumero tambin garantiza que no realizar variacin o cambio alguno sobre los elementos apuntados por ese puntero, ya que tambin define ese parmetro como const: short isNumero(const char* , short); De lo contrario, esa llamada a la funcin isNumero no hubiera sido permitida por el compilador.

509

Informtica Aplicada. Programacin en Lenguaje C.

17.8.

Necesitamos una funcin que nos diga si, en un determinado array de tipo long, est almacenado un valor concreto y, en tal caso, que nos diga en qu posicin del array est. Por ejemplo, si tenemos el array long a[10] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20}; Y queremos saber si est el valor 8, la funcin debera indicarnos como salida el valor 3, que es el ndice que marca la posicin donde se encuentra el 8. A la funcin la llamaremos IndiceArray. La funcin deber recibir tres parmetros: la direccin del array, la dimensin del array, y el valor buscado. La funcin debe devolver el ndice que indica la posicin del elemento en el array; si el elemento no est en el array, deber devolver el valor -1. Indique el prototipo de esta funcin y escriba una posible definicin de la funcin IndiceArray().

Como prototipo de la funcin, podemos tener (se proponen dos opciones igualmente vlidas): long IndiceArray(const long*, long, long); short IndiceArray(const long*, short, long); Y un posible cdigo para esta funcin podra ser la siguiente: short IndiceArray(const long* a , short d, long el) { int i; for(i = 0 ; i < d ; i++) if(a[i] == el) return i; return -1; } En cuanto encuentre el elemento abandona la bsqueda y devuelve el ndice actual. Si no encuentra el elemento devuelve el valor -1.

510

Captulo 17. Funciones y parmetros con punteros.

17.9.

Suponga que utilizamos un array de 10 elementos para almacenar los coeficientes de un polinomio que podr ser hasta de grado 9. En la posicin 0 del array almacenamos el coeficiente independiente; en la posicin 1, el coeficiente de x, en la posicin 2 en coeficiente de , y en general, en la . posicin i almacenamos el coeficiente de

Se necesita una funcin que reciba como primer parmetro ese array (de tipo double); como segundo parmetro otro array de 10 elementos donde la funcin deber dejar los coeficientes que corresponden a la derivada del polinomio primero. Indique cul es el prototipo de esta funcin (llmela derivar). Tenga en cuenta que no es necesario que esta funcin devuelva valor alguno. Y escriba el cdigo de la funcin.

El algoritmo para este sencillo ejercicio ya lo hemos presentado en el captulo 10. Ahora se trata de resolverlo utilizando una funcin, que puede tener el siguiente aspecto: void derivar(const double* a, double* b) { short i; for(b[9] = 0 , i = 0 ; i < 9 ; i++) b[i] = a[i + 1] * (i + 1); } Y cuyo prototipo es, como se ve: void derivar(const double*, double*); La funcin derivar no cambia el valor de ningn elemento del array a recibido como primer parmetro. Por eso, hemos declarado ese puntero como puntero a constante.

511

Informtica Aplicada. Programacin en Lenguaje C.

17.10.

Escriba el cdigo de una funcin (que llamar cont_alfanum()) que reciba como primer parmetro una cadena de texto y como segundo parmetro el tamao en que esta cadena ha sido declarada y que devuelva el nmero de caracteres alfabticos o numricos. Por ejemplo, el siguiente cdigo: char cad[] = Hoy es 09/02/2010.; printf(%hd, cont_alfanum(cad, strlen(cad))); muestra por pantalla el valor 13.

Es un ejercicio muy sencillo una vez se ha aprendido a manejar las cadenas de texto. #include <ctype.h> short cont_alfanum(const char*a, short d) { short i, cont; for(i = 0 , cont = 0 ; i < d && a[i] ; i++) if(isalnum(*(a + i))) cont++; return cont; }

17.11.

Serie de Fibonacci. Fibonacci fue un matemtico italiano del siglo XIII que descubri la serie que lleva su nombre. Cada nmero de esa serie es el resultado de la suma de los dos anteriores: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, ... Haga un programa que calcule la serie de Fibonacci y guarde los 40 primeros elementos de la serie en un array de tipo unsigned long. (Tenga en cuenta que el elemento 46 de la serie de Fibonacci es el mayor elemento de la serie codificable en un

512

Captulo 17. Funciones y parmetros con punteros.

entero sin signo de 32 bits: si calcula valores ms all de esta posicin, debera definir un tipo de dato nuevo que albergara enteros ms largos.) Esta serie goza de una serie de propiedades curiosas. Por ejemplo: La suma de los n primeros trminos verifica que:

f
i 1 n

fn2 1

La suma de los n primeros trminos pares verifica que:

f
i 1

2i

f2n1 1

La suma de los n primeros trminos impares verifica que:

f
i 1

2i 1

f2n

La suma de los cuadrados de los n primeros trminos verifica que:

f
i 1

fn fn1

Si n es divisible por m, entonces fn es divisible por fm . Cualesquiera dos elementos consecutivos de la serie de Fibonacci son primos entre s. El cociente de dos nmeros consecutivos de la serie se aproxima al nmero ureo: fn1 fn cuando n . Contine el programa anterior de forma que el usuario pueda solicitar mediante un men de opciones, la comprobacin de cada una de estas propiedades...

513

Informtica Aplicada. Programacin en Lenguaje C.

Antes de desanimarse ante la longitud de este ejercicio propuesto y dejarlo estar, vale la pena que al menos considere que es un ejemplo muy sencillo, con funciones simples, todas ellas muy parecidas. Y que si bien es cierto que es mucho cdigo para copiar en un ordenador y ver cmo funciona, s se puede detener en una o dos de las 9 funciones definidas, y verlas en funcionamiento. Un posible cdigo que resuelve el problema planteado podra ser el siguiente:

/* ======================================================= */ /* PROGRAMA DE LA SERIE DE FIBONACCI. */ /* ======================================================= */ #include <stdio.h> #include <stdlib.h> #include <math.h> #define RANGO 40 // Declaracin de las funciones... char menu(void); void fib01(unsigned long*); void fib02(unsigned long*); void fib03(unsigned long*); void fib04(unsigned long*); void fib05(unsigned long*); void fib06(unsigned long*); long mcd(long, long); void fib07(unsigned long*); void fib08(unsigned long*); // Funcin principal... int main(void) { unsigned long fib[RANGO + 1]; // No utilizaremos el elemento 0. unsigned short i; char opcion; fib[1] = 1; fib[2] = 1; // Serie de fibonacci... for(i = 3 ; i <= RANGO ; i++)

514

Captulo 17. Funciones y parmetros con punteros.

fib[i] = fib[i - 1] + fib[i - 2]; do { // Men de opciones ... opcion = menu(); switch(opcion) { case '1': fib01(fib); break; case '2': fib02(fib); break; case '3': fib03(fib); break; case '4': fib04(fib); break; case '5': fib05(fib); break; case '6': fib06(fib); break; case '7': fib07(fib); break; case '8': fib08(fib); break; case '0': printf("\nFin del programa"); printf("\nPulse tecla para terminar"); flushall(); getchar(); break; default: printf("opcin no definida ... "); } }while(opcion != '0'); return 0; } /* ------------------------------------------------------- */ /* Definicin de las funciones */ /* ------------------------------------------------------- */ /* Funcin que muestra el men de opciones char menu(void) { char opcion; printf("\n\n\tPROPIEDADES printf("\n\n\t0. Salir de printf("\n\t1. La suma de printf("\n\t es igual a DE LA SERIE DE FIBONACCI"); programa."); los n primeros terminos"); f[n + 2] - 1."); */

printf("\n\t2. La suma de los n primeros terminos pares (de 2 a 2*n)"); printf("\n\t es igual a f[2 * n + 1] - 1."); printf("\n\t3. La suma de los n primeros terminos impares (de 1 a 2*n-1)"); printf("\n\t es igual a f[n + 2].");

515

Informtica Aplicada. Programacin en Lenguaje C.

printf("\n\t4. La suma de los cuadrados de los n primeros terminos"); printf("\n\t es igual a f[n] * f[n + 1]."); printf("\n\t5. Si n es divisible por m, entonces f[n]"); printf("\n\t es divisible por f[m]."); printf("\n\t6. Cualquier par consecutivo de fibonacci"); printf("\n\t son primos entre si."); printf("\n\t7. El cociente de dos elementos consecutivos de la serie"); printf("\n\t se aproxima al numero aureo."); printf("\n\t8. Mostrar los 40 primeros elementos de la serie de Fibonacci."); printf("\n\n\n\tElija una opcion ... "); do opcion = getchar(); while(opcion < '0' || opcion > '8'); return opcion; } // Funcin fib01 void fib01(unsigned long*f) { unsigned short n, i; unsigned long S = 0; printf("\n\n\n\tOPCION SELECCIONADA 1."); printf("\n\n\tLa suma de los n primeros terminos"); printf("\n\tes igual a f[n + 2] - 1."); printf("\n\nIndique el indice n que quiere verificar (entre 1 y 38) ... "); scanf( %hu",&n); if(n > RANGO - 2) printf("No se disponen de suficientes elementos."); else if(n == 0) printf("No valido"); else { for(i = 1 ; i <= n ; i++) S += *(f + i); printf("\n\nSUMA f[ 1]... f[%2hu] = %lu\n",n,S); printf("f[%hu]-1 = %lu\n",n + 2,*(f + n + 2)-1); }

516

Captulo 17. Funciones y parmetros con punteros.

} // Funcin fib02 void fib02(unsigned long*f) { unsigned short n, i; unsigned long S = 0; printf("\n\n\n\tOPCION SELECCIONADA 2."); printf("\n\tLa suma de los n primeros terminos pares (2, 4, ..., 2 * n)"); printf("\n\tpares es igual a f[2 * n + 1] - 1."); printf("\n\nIndique el indice n que quiere verificar (entre 1 y 19) ... "); scanf( %hu",&n); // n no puede ser tal que 2 * n + 1 sea mayor que RANGO... if(n > (RANGO - 1) / 2) printf("No se disponen de suficientes elementos."); else if(n == 0) printf("No valido"); else { for(i = 2 ; i <= 2 * n ; i += 2) S += *(f + i); // Mostramos resultados... printf("\n\nSUMA f[ 2]... f[%2hu] = %lu\n",2 * n,S); printf("f[%hu] - 1 = %lu\n", 2 * n + 1,*(f + 2 * n + 1) - 1); } } // Funcin fib03 void fib03(unsigned long*f) { unsigned short n, i; unsigned long S = 0; printf("\n\n\n\tOPCION SELECCIONADA 2."); printf("\n\tLa suma de los n primeros terminos impares (de 1 a 2*n-1)"); printf("\n\tes igual a f[n + 2]."); printf("\n\nIndique el indice n que quiere verificar (entre 1 y 20) ... "); scanf( %hu",&n); // n no puede ser tal que 2 * n - 1 sea mayor que RANGO... if(n > (RANGO + 1) / 2) printf("No se disponen de suficientes

517

Informtica Aplicada. Programacin en Lenguaje C.

elementos."); else if(n == 0) printf("No valido"); else { for(i = 1 ; i <= 2 * n - 1 ; i += 2) S += *(f + i); // Mostramos resultados... printf("\n\nSUMA f[ 2]... f[%2hu] = %lu\n", 2 * n,S); printf("f[%hu] = %lu\n",2 * n,*(f + 2 * n)); } } // Funcin fib04 void fib04(unsigned long*f) { unsigned short n, i; unsigned long S = 0; printf("\n\n\n\tOPCION SELECCIONADA 3."); printf("\n\tLa suma de los cuadrados de los n primeros terminos"); printf("\n\tes igual a f[n] * f[n + 1]."); printf("\n\nIndique el indice n que quiere verificar (entre 1 y 39) ... "); scanf( %hu",&n); if(n > RANGO - 1) printf("No se disponen de suficientes elementos."); else if(n == 0) printf("No valido"); else { for(i = 1 ; i <= n ; i++) S += *(f + i) * *(f + i); printf("\n\nSUMA f[ 1]^2... f[%2hu]^2 = %lu\n", n,S); printf("f[%hu] * f[%hu] = %lu\n", n,n + 1,*(f + n) * *(f + n + 1)); } } // Funcin fib05 void fib05(unsigned long*f) { unsigned short n, m; printf("\n\n\n\tOPCION SELECCIONADA 5."); printf("\n\tSi n es divisible por m, entonces f[n]"); printf("\n\tes divisible por f[m].\n\n");

518

Captulo 17. Funciones y parmetros con punteros.

for(n = 3 ; n < RANGO / 2 ; n++) // Todos los mltiplos de i ... { /* Comenzamos con n = 3, porque f[2] = 1, y todos sern mltiplos de 1. */ printf("\n\n** n = %2hu\tf[%2hu] = %10lu ** \n", n,n,*(f + n)); for(m = 2 * n ; m < RANGO ; m += n) { printf(" m = %2hu\tf[%2hu] = %10lu\t", m, m, *(f + m)); printf("f[%2hu] %% f[%2hu] = %lu", m,n,*(f + m) % *(f + n)); if(*(f + m) % *(f + n) == 0) printf("\t[DIVISIBLE]\n"); } printf("\n\n\tPulse una tecla para ver siguiente serie de multiplos ... "); getchar(); } } // Funcin fib06 void fib06(unsigned long*f) { unsigned short n; printf("\n\n\n\tOPCION SELECCIONADA 6."); printf("\n\tCualquier par consecutivo de fibonacci"); printf("\n\tson primos entre si.\n\n"); for(n = 3 ; n < RANGO ; n++) { printf("f[%2hu] = %10lu\t",n,*(f + n)); printf("f[%2hu] = %10lu\t",n + 1, *(f + n + 1)); printf("MCD = %lu\t",mcd(*(f + n),*(f + n + 1))); if(mcd(*(f + n),*(f + n + 1)) == 1) printf("[COPRIMOS]\n"); } } // Funcin mcd, definida para uso de fib06 long mcd(long a, long b) { return b ? mcd(b,a % b) : a; } // Funcin fib07 void fib07(unsigned long*f) {

519

Informtica Aplicada. Programacin en Lenguaje C.

unsigned short n; const double oro = (1 + sqrt(5)) / 2; char blancos[] = "

";

printf("\n\n\n\tOPCION SELECCIONADA 7."); printf("\n\tEl cociente de dos elementos consecutivos de la serie"); printf("\n\tse aproxima al numero aureo.\n\n"); for(n = 1 ; n < RANGO ; n++) { printf("f[%2hu] = %10lu\t",n,*(f + n)); printf("f[%2hu] = %10lu\t",n + 1, *(f + n + 1)); printf("COCIENTE = %20.18lf\n" (double)*(f + n + 1) / *(f + n)); } printf("\n%sNumero de oro (1+sqrt(5)) / 2 -> %20.18lf", blancos,oro); } // Funcin fib08 void fib08(unsigned long*f) { unsigned short n; printf("\n\n\n\tOPCION SELECCIONADA 8."); printf("\n\tMostrar los 40 primeros elementos de la serie de Fibonacci.\n\n"); for(n = 1 ; n <= RANGO ; n++) printf("f[%2hu] = %10lu%s", n,*(f + n),n % 2 ? "\t" : "\n"); }

520

CAPTULO 18
ASIGNACIN DINMICA DE MEMORIA.
Cuando hemos hablado de los vectores, hemos remarcado la idea de que la dimensin indicada en su declaracin debe ser un literal: long numeros[100]; Con esa sentencia creamos un vector de 100 enteros largos: acabamos de reservar 400 bytes para codificar valores de este tipo de dato. En muchas ocasiones nos podra interesar que la declaracin de este vector se hiciera mediante una variable, y poder as reservar el espacio de memoria que realmente necesitramos. Por ejemplo, al crear unas matrices en un programa de operatoria de matrices, vendra bien poder codificar algo del siguiente estilo: long f, c; printf(Nmero de filas de su matriz A ... ); scanf( %ld,&f); printf(Nmero de columnas de su matriz A ... ); scanf( %ld,&c);

Informtica Aplicada. Programacin en Lenguaje C.

long A[f][c]; Y as, quedara creada la matriz de acuerdo con la dimensin deseada. Pero este cdigo era errneo en el estndar C90, y lo sigue siendo cuando programamos en C++. Esta forma de crear matrices no puede llevarse a cabo bajo ningn concepto, porque la reserva de memoria de una matriz o de un vector se realiza en tiempo de compilacin: el compilador, antes de ejecucin alguna, debe saber cunta memoria se debe reservar y para qu dominio de valores. El estndar C99 s admite este modo de declarar arrays. Pero, como ya se explic en el captulo 11 al hablar de los arrays, no hacemos uso de esta valiosa posibilidad en el cdigo de este manual. En el estndar C90, si deseamos hacer un programa que necesite manejar informacin con arrays o matrices, siempre habr que dimensionar esos espacios de memoria de manera que consideren el caso ms desfavorable: para la ocasin en que se necesite un mayor tamao. Y luego recorrer ese array o esa matriz acotando el uso a la zona que nos interesa. Un ejemplo simple: si queremos crear una matriz cuadrada, y su dimensin puede estar en un valor entre 2 y 100, el programador deber hacer lo siguiente: long matriz[100][100]; short int dimension; short int i, j; printf(Indique la dimensin de la matriz ... ); scanf( %hd,&dimension); // Introduccin de valores... for( i = 0 ; i < dimension ; i++) for(j = 0 ; j < dimension j++) { printf(matriz[%hd][%hd] = , i, j); scanf( %ld,&matriz[i][j]); } Y as, si el usuario introduce el valor 3, entonces emplearemos 36 bytes para el manejo de nuestra matriz de 9 enteros largos. Pero la memoria reservada ser de cuarenta mil bytes.

522

Captulo 18. Asignacin dinmica de memoria.

C dispone de un modo para lograr que la reserva de memoria quede optimizada, haciendo esa reserva en tiempo de ejecucin, cuando el programa se ejecuta y ya sabe con exactitud la memoria que necesita: mediante la asignacin dinmica de memoria. Con ello se logra ajustar la cantidad de memoria que utiliza el programa al tamao que realmente es necesario para cada ejecucin concreta Disponemos de algunas funciones que nos permiten reservar y liberar esa memoria de una forma dinmica, es decir, en tiempo de ejecucin.

Funcin malloc.
Esta funcin malloc queda definida en la biblioteca stdlib.h. Tambin la encontramos en la biblioteca alloc.h. Recomendamos hacer uso siempre de la definicin recogida en stdlib. Su prototipo es el siguiente: void *malloc(size_t size); Donde el tipo de dato size_t lo consideraremos ahora nosotros simplemente como un tipo de dato long. La funcin reserva tantos bytes consecutivos como indique el valor de la variable size y devuelve la direccin del primero de esos bytes. Esa direccin debe ser recogida por una variable puntero: si no se recoge, tendremos la memoria reservada pero no podremos acceder a ellas porque no sabremos dnde ha quedado hecha esa reserva. Como se ve, la direccin de devuelve con el tipo de dato void*. La funcin malloc as lo hace porque est definida para hacer reserva de bytes, al margen de para qu se reservan esos bytes. Pero no tendra sentido trabajar con direcciones de tipo de dato void. Por eso, en la asignacin al puntero que debe recoger la direccin del array, se debe indicar, mediante el operador forzar tipo, el tipo e la direccin. Veamos un ejemplo:

523

Informtica Aplicada. Programacin en Lenguaje C.

long dim; float *vector; printf(Dimensin de su vector ...); scanf( %ld,&dim); vector = (float*)malloc(4 * dim); En el ejemplo, hemos reservado tantos bytes como hace falta para reservar memoria para un array de float de la dimensin indicada por el usuario. Como cada variable float ocupa cuatro bytes hemos multiplicado la dimensin por cuatro. Como se ve, en la asignacin, a la direccin de memoria que devuelve la funcin malloc, le hemos indicado que deber comportarse como direccin de float. De ese tipo es adems el puntero que la recoge. A partir de este momento, desde el puntero vector y con operatoria de punteros podemos manejarnos por el array creado en tiempo de ejecucin. Tambin se puede recorrer el array con ndices, como si de un vector normal se tratase: la variable vector[i] es la misma que la variable *(vector + i). Es responsabilidad del programador reservar un nmero de bytes que sea mltiplo del tamao del tipo de dato para el que hacemos la reserva. Si, por ejemplo, en una reserva para variables float, el nmero de bytes no es mltiplo de 4, el compilador no interrumpir su trabajo, y generar el ejecutable; pero existe el peligro, en un momento dado, de incurrir en una violacin de memoria. De forma habitual al invocar a la funcin malloc se hace utilizando el operador sizeof. La sentencia anterior en la que reservbamos espacio para nuestro vector de tipo float quedara mejor de la siguiente forma: vector = (float*)malloc(sizeof(float) * dim); Y una ltima observacin sobre la reserva de la memoria. La funcin malloc busca un espacio de memoria (en la memoria heap o montn: pero no es ese tema que ahora vaya a ocuparnos) de la longitud que se le indica en el argumento. Gracias a esta asignacin de memoria se podr trabajar con una serie de valores, y realizar clculos necesarios

524

Captulo 18. Asignacin dinmica de memoria.

para nuestra aplicacin. Pero y si no hay en la memoria un espacio suficiente de bytes consecutivos, libres y disponibles para satisfacer la demanda? En ese caso, no podramos realizar ninguna de las tareas que el programa tiene determinadas, simplemente porque no tendramos memoria. Cuando la funcin malloc lo logra satisfacer la demanda, devuelve el puntero nulo. Es importante, siempre que se crea un espacio de memoria con esta funcin, y antes de comenzar a hacer uso de ese espacio, verificar que s se ha logrado hacer buena reserva. En caso contrario, habitualmente lo que habr que hacer es abortar la ejecucin del programa, porque sin memoria, no hay datos ni capacidad de operar con ellos. Esta verificacin se realiza de la siguiente manera: vector = (float*)malloc(dimension * sizeof(float)); if(vector == NULL) { printf("\nNo hay memoria disponible."); printf("\nEl programa va a terminar."); printf("\nPulse cualquier tecla ... "); exit(1); } Y as, nunca trabajaremos con un puntero cuya direccin es nula. Si no hiciramos esa verificacin, en cuanto se echase mano del puntero vector para recorrer nuestra memoria inexistente, el programa abortara inmediatamente. La funcin exit termina la ejecucin del programa; quedar presentada en el siguiente captulo sobre funciones; est definida en el archivo stdlib.h. Es mejor tomar nosotros la iniciativa, mediante la funcin exit, y decidir nosotros el modo de terminacin del programa en lugar de que lo decida un error fatal de ejecucin. Existen otras funciones muy similares de reserva de memoria dinmica. Por ejemplo, la funcin calloc, que tiene el siguiente prototipo: void *calloc(size_t nitems, size_t size);

525

Informtica Aplicada. Programacin en Lenguaje C.

Que recibe como parmetros el nmero de elementos que se van a reservar y el nmero de bytes que ocupa cada elemento. La sentencia anterior vector = (float*)malloc(dimension * sizeof(float)); ahora, con la funcin calloc, quedara: vector = (float*)calloc(dimension, sizeof(float)); Como se ve, en sustancia, ambas funciones tienen un comportamiento muy similar. Tambin en este caso, obviamente, ser conveniente hacer siempre la verificacin de que la memoria ha sido felizmente reservada. Lo mejor es acudir a las ayudas de los compiladores para hacer buen uso de las especificaciones de cada funcin. Una ltima funcin de asignacin dinmica de la memoria es la funcin realloc. Su prototipo es el siguiente: void *realloc(void *block, size_t size); Esta funcin sirve para reasignar una zona de memoria sobre un puntero. El primer parmetro es el del puntero sobre el que se va a hacer el realojo; el segundo parmetro recoge el nuevo tamao que tendr la zona de memoria reservada. Si el puntero block ya tiene memoria asignada, mediante una funcin malloc o calloc, o reasignada por otra llamada anterior realloc, entonces la funcin vara el tamao de la posicin de memoria al nuevo tamao que indica la variable size. Si el tamao es menor que el que haba, simplemente deja liberada para nuevos usos la memoria de ms que antes disponamos y de la que hemos decidido prescindir. Si el nuevo tamao es mayor, entonces procura prolongar ese espacio reservado con los bytes siguientes; si eso no es posible, entonces busca en la memoria un espacio libre del tamao indicado por size, y copia los valores asignados en el tramo de memoria anterior en la nueva direccin. La funcin devuelve la direccin donde queda ubicada toda la

526

Captulo 18. Asignacin dinmica de memoria.

memoria reservada. Es importante recoger esa direccin de memoria que devuelve la funcin realloc, porque en su ejecucin, la funcin puede cambiar la ubicacin del vector. La llamada podra ser as: vector = (float*)realloc(vector, new_dim * sizeof(float)); Si el puntero block tiene el valor nulo, entonces realloc funciona de la misma forma que la funcin malloc. Si el valor de la variable size es cero, entonces la funcin realloc libera el puntero block, que queda nulo. En ese caso, el comportamiento de la funcin realloc es semejante al de la funcin free que vemos a continuacin.

Funcin free.
Esta funcin viene tambin definida en la biblioteca stdlib.h. Su cometido es liberar la memoria que ha sido reservada mediante la funcin malloc, o calloc, o realloc. Su sintaxis es la siguiente: void free(void *block); Donde block es un puntero que tiene asignada la direccin de memoria de cualquier bloque de memoria que haya sido reservada previamente mediante una funcin de memoria dinmica, como por ejemplo la funcin malloc ya presentada y vista en el epgrafe anterior. La memoria que reserva el compilador ante una declaracin de un vector se ubica en la zona de variables de la memoria. La memoria que se reserva mediante la funcin malloc queda ubicada en otro espacio de memoria distinto, que se llama habitualmente heap. En ningn caso se puede liberar, mediante la funcin free, memoria que haya sido creada esttica, es decir, vectores declarados como tales en el programa y que reservan la memoria en tiempo de ejecucin. Es esa memoria del heap la que libera la funcin free. Si se aplica esa funcin sobre memoria esttica se produce un error en tiempo de compilacin.

527

Informtica Aplicada. Programacin en Lenguaje C.

Ejemplo: la Criba de Erastthenes.


Supongamos que deseamos hacer un programa que almacene en un vector todos los nmeros primos menores que un milln. Para esa bsqueda utilizaremos el algoritmo de la criba de Erastthenes. Es un algoritmo que permite encontrar todos los Nmeros Primos menores o iguales a un entero dado. Se comienza generando una tabla con todos los nmeros desde 1 hasta el lmite superior (en nuestro caso hemos quedado que un milln). Tomamos el nmero 1 como primo por definicin. A continuacin se pasa al siguiente nmero, que es el 2, que ya desde ese momento se considerar primo, y se procede a marcar en la tabla como enteros compuestos (es decir, no primos) a todos los nmeros posteriores a 2 y mltiplos de 2. A continuacin pasamos al siguiente nmero que no est marcado como compuesto, que resulta ser el 3, que queda considerado ahora como primo, y procedemos a marcar como compuestos en nuestra tabla todos los nmeros posteriores a l y mltiplos suyos. Ya hemos marcado como compuestos todos los mltiplos de 3. Ahora buscamos el siguiente nmero no marcado como compuesto. El 4 es mltiplo de 2 y ya ha quedado marcado como compuesto, as que nos lo saltamos y llegamos al 5 que no es mltiplo ni de 2 ni de 3. El 5 queda considerado primo y ahora procedemos a todos los mltiplos de 5. El proceso se va repitiendo hasta llegar al ltimo nmero menor que el lmite superior marcado. Todos los nmeros que no hayan sido marcados como compuestos sern primos. En realidad no es necesario realizar la criba hasta llegar al ltimo nmero menor que el lmite superior fijado: basta llegar hasta la raz cuadrada de ese lmite. La razn es que si un nmero no tiene un divisor marcar como compuestos

528

Captulo 18. Asignacin dinmica de memoria.

menor que su raz cuadrada, entonces tampoco lo puede tener mayor que su raz cuadrada y, por tanto, si al llegar a ese limite no se ha encontrado un divisor, entonces ese nmero es primo con total certeza. Una vez tenemos claro el algoritmo que nos permitir llegar a crear la tabla de los primos, el siguiente paso ser implementar el programa que nos haga este proceso.

Matrices en memoria dinmica.


Para crear matrices, podemos trabajar de dos maneras diferentes. La primera es crear una estructura similar a la indicada en el cuadro 17.1. Presentamos un cdigo que genera una matriz de tipo float. El programa solicita al usuario las dimensiones de la matriz (el nmero de filas y el nmero de columnas). A continuacin la aplicacin reserva un array de tantos punteros como filas tenga la matriz; y sobre cada uno de esos punteros hace una reserva de tantos elementos float como columnas se deseen en la matriz. Luego, para ver cmo se puede hacer uso de esa matriz creada mediante memoria dinmica, solicitamos al usuario que d valor a cada elemento de la matriz y, finalmente, mostramos por pantalla todos los elementos de la matriz. Por ltimo, se realiza la liberacin de la memoria. Para ello primero habr que liberar la memoria asignada a cada uno de los vectores creados en memoria dinmica sobre el puntero p, y posteriormente liberar al puntero p del array de punteros. El cdigo es el siguiente: #include <stdlib.h> #include <stdio.h> int main(void) { float **p; short f, c;

529

Informtica Aplicada. Programacin en Lenguaje C.

short i, j; // Introducir las dimensiones ... printf("Indique filas del vector ... "); scanf( %hd",&f); printf("Indique columnas del vector ... "); scanf( %hd",&c); // Creacin de las filas ... p = (float**)malloc(f * sizeof(float*)); if(p == NULL) { printf("Memoria insuficiente.\n"); printf("La ejecucin se interrumpir.\n"); printf("Pulse una tecla para terminar ... "); getchar(); exit(0); } // Creacin de las columnas ... for( i = 0 ; i < f ; i++) { *(p + i) = (float*)malloc(c * sizeof(float)); if(*(p + i) == NULL) { printf("Memoria insuficiente.\n"); printf("La ejecucin se interrumpir.\n"); printf("Pulse una tecla para terminar..."); getchar(); exit(0); } } // Asignacin de valores ... for(i = 0 ; i < f ; i++) for(j = 0 ; j < c ; j++) { printf("matriz[%2hd][%2hd] = ", i, j); scanf( %f",*(p + i) + j); } // Mostrar la matriz por pantalla ... for(i = 0 ; i < f ; i++) { printf("\n"); for(j = 0 ; j < c ; j++) printf("%6.2f\t",*(*(p + i) + j)); } // Liberar la memoria ... for(i = 0 ; i < f ; i++)

530

Captulo 18. Asignacin dinmica de memoria.

free(*(p + i)); free(p); return 0; } Desde luego, hemos trabajado con operatoria de punteros. Ya se explic que hablar de *(*(p + i) + j) es lo mismo que hablar de p[i][j]. Y que hablar de (*(p + i) + j) es hablar de &p[i][j]. Y as se podra haber codificado. Veamos algunas de las lneas del cdigo anterior, recogidas con operatoria de ndices: // Asignacin de valores ... for(i = 0 ; i < f ; i++) for(j = 0 ; j < c ; j++) { printf("matriz[%2hd][%2hd] = ", i, j); scanf( %f",&p[i][j]); } // Mostrar la matriz por pantalla ... for(i = 0 ; i < f ; i++) { printf("\n"); for(j = 0 ; j < c ; j++) printf("%6.2f\t",p[i][j]); } Una ltima observacin a este cdigo presentado: el puntero p es (debe ser as) puntero a puntero. Y, efectivamente, l apunta a un array de punteros que, a su vez, apuntan a un array de elementos float. Decamos que haba dos formas de crear una matriz por asignacin dinmica de memoria. La segunda es crear un solo array, de longitud igual al producto de filas por columnas. Y si la matriz tiene columnas, considerar los primera fila de la matriz; y los segundos fila, y as, hasta llegar a la ltima fila. El cdigo de esta nueva forma de manejar matrices podra ser: #include <stdlib.h> #include <stdio.h> int main(void) { float *p; filas y primeros elementos del vector como la elementos, como la segunda

531

Informtica Aplicada. Programacin en Lenguaje C.

short f, c; short i, j; // Introducir las dimensiones ... printf("Indique filas del vector ... "); scanf( %hd",&f); printf("Indique columnas del vector ... "); scanf( %hd",&c); // Creacin de la matriz ... p = (float*)malloc(f * c * sizeof(float*)); if(p == NULL) { printf("Memoria insuficiente.\n"); printf("La ejecucin se interrumpir.\n"); printf("Pulse una tecla para terminar ... "); getchar(); exit(0); } // Asignacin de valores ... for(i = 0 ; i < f ; i++) for(j = 0 ; j < c ; j++) { printf("matriz[%2hd][%2hd] = ", i, j); scanf( %f",p + (i * c + j)); } // Mostrar la matriz por pantalla ... for(i = 0 ; i < f ; i++) { printf("\n"); for(j = 0 ; j < c ; j++) printf("%6.2f\t",*(p + (i * c + j))); } // Mostrar los datos como vector lineal ... printf("\n\n"); for(i = 0 ; i < f * c ; i++) printf("%6.2f\t",*(p + i)); // Liberar la memoria ... free(p); return 0; } Ahora el puntero es un simple puntero a float. Y jugamos con los valores de los ndices para avanzar ms o menos en el array. Cuando hablamos de *(p + (i * c + j)), donde p es el puntero, i es el contador

532

Captulo 18. Asignacin dinmica de memoria.

de filas, j el contador de columnas, y c la variable que indica cuntas columnas hay, estamos recorriendo el array de la siguiente manera: si queremos ir, por ejemplo, a la fila 2 (i = 2) y a la columna 5 (j = 5), y suponiendo que la matriz tiene, por ejemplo, 8 columnas (c = 8) entonces, ese elemento del vector (2, 5) est ubicado en la posicin 2 * 8 + 5. es decir, en la posicin 21. Con el valor i = 0 tenemos los elementos de la primera fila, situados en el vector desde su posicin 0 hasta el su posicin c 1. Con el valor i = 1 tenemos los elementos de la segunda fila, situados en el vector desde su posicin c hasta su posicin 2 * c 1. Y en general, la fila i se sita en el vector desde la posicin i * c hasta la posicin (i + 1) * c 1. De nuevo, podremos trabajar con operatoria de ndices: hablar de *(p + (i * c + j)) es lo mismo que hablar del elemento del vector p[i * c + j].

Ejercicios.

18.1.

Escriba un programa que cree un vector con todos los primos menores que un milln Utilice, para la bsqueda de los primos, la criba de Erastthenes.

Vamos a definir para ellos dos funciones ms la funcin principal: #include <stdio.h> #include <math.h> #include <stdlib.h> #define MAX 1000000 long Criba(char*, long); void TablaPrimos(char*, long*, long); int main(void) { char *num; long *primos;

533

Informtica Aplicada. Programacin en Lenguaje C.

long pr, i; num = (char*)malloc((MAX + 1) * sizeof(char)); if(num == NULL) { printf("\nNo hay memoria disponible."); printf("\nEl programa va a terminar."); printf("\nPulse cualquier tecla ... "); exit(1); } pr = Criba(num, MAX + 1); // Ya est hecha la criba. Tenemos pr primos. // Creamos ahora el vector que contendr a los primos. // Reservamos memoria para este vector: primos = (long*)malloc((pr + 1) * sizeof(long)); if(primos == NULL) { printf("\nNo hay memoria disponible."); printf("\nEl programa va a terminar."); printf("\nPulse cualquier tecla ... "); exit(1); } TablaPrimos(num, primos, MAX + 1); free(num); // Mostramos los primos por pantalla ... printf("Primos menores que %ld... \n\n",MAX); for(i = 0 ; *(primos + i) != 0 ; i++) printf("%10ld", *(primos + i)); // Liberamos la memoria ... free(primos); return 0; } long Criba(char* num, long rango) { long i, j; // En principio marcamos todos los elementos como PRIMOS for(i = 0 ; i < rango; i++) num[i] = 'p'; for(i = 2 ; i < sqrt(rango) ; i++) if(num[i] == 'p') for(j = 2 * i ; j < rango ; j += i) num[j] = 'c'; for( i = 1 , j = 0 ; i < rango ; i++) if(num[i] == 'p') j++; return j; }

534

Captulo 18. Asignacin dinmica de memoria.

void TablaPrimos(char* num, long* primos, long rango) { long i, j; for(i = 1 , j = 0 ; i < rango ; i++) if(num[i] == 'p') { *(primos + j) = i; j++; } *(primos + j) = 0; } Vamos viendo este programa, resuelto mediante funciones. Primero reservamos, en la funcin main, un espacio de memoria suficiente para albergar tantas variables char (de un bytes cada una) como indica MAX + 1. MAX es la macro que recoge el lmite superior sobre el que se van a buscar todos los primos inferiores. A esa memoria, que manejaremos desde un puntero tipo char* que hemos llamado num, le asignamos a todas sus posiciones el valor p, que vamos a entender que significa primo. Y es sobre ese vector sobre quien se hace la criba de Erastthenes, definida en la funcin Criba, que devuelve el nmero de primos que hay en ese rango entre 1 y MAX (la funcin principal recoge ese valor en la variable pr) y que deja modificada la memoria recogida por el puntero num: una vez ejecutada la funcin Criba, cada posicin valdr c p segn que su ndice en el vector sea compuesto o primo: num[i] valdr p si i es primo, y valdr c si i en compuesto. Como ya sabemos cuntos primos hay en nuestro intervalo, ahora creamos un espacio de memoria para almacenar enteros largos: tantos como indica la variable pr (en realidad reservamos uno ms que pr, por el motivo que se explica enseguida). Y llamando a la funcin TablaPrimos se le asigna a cada posicin de esa memoria cada uno de los pr primos del intervalo. El ltimo valor (ese espacio en memoria de ms que acabamos de reservar) de nuestro vector de primos lo ponemos a cero: as el recorrido de nuestro vector no se rige por un ndice, sino por un valor que hace de fin de lista: se podr recorrer el vector de enteros primos

535

Informtica Aplicada. Programacin en Lenguaje C.

mientras que no se llegue a un valor cero. As se ha recorrido al final de funcin principal, cuando se muestra la lista de primos por pantalla. Si en el programa se quiere aumentar el rango de primos bastar modificar la definicin de la directiva define. Al final del programa, y antes de terminar su ejecucin liberamos la memoria de la tabla de primos. Antes, ya habamos liberado la memoria recogida por el puntero num. Y eso es algo interesante a destacar de la memoria dinmica: se crea cuando se necesita, y se libera cuando ya no se necesita. Y en ese aspecto, esta memoria tiene un rgimen de vida y de mbito diferente al de las variables creadas por declaracin. La variable puede dejar de existir antes de finalizar el bloque en el que ha sido creada; y puede comenzar a existir pasado un tiempo al inicio de la ejecucin de su bloque.

18.2.

Un cuadro mgico es un reticulado de n filas y n columnas que tiene la propiedad de que todas sus filas, y todas sus columnas, y las diagonales principales, suman el mismo valor. Por ejemplo: 6 7 2 1 5 9 8 3 4

La tcnica que se utiliza para generar cuadros mgicos (que tienen siempre una dimensin impar: impar nmero de filas y de columnas) es la siguiente: a. Se comienza fijando el entero 1 en el espacio central de la primera fila. b. Se van escribiendo los sucesivos nmeros (2, 3, ...)

536

Captulo 18. Asignacin dinmica de memoria.

sucesivamente, en las casillas localizadas una fila arriba y una columna a la izquierda. Estos desplazamientos se realizan tratando a la matriz como si estuviera envuelta sobre s misma, de forma que moverse una posicin hacia arriba desde la fila superior lleva a la fila inferior, y moverse una posicin a la izquierda desde la primera columna lleva a la columna ms a la derecha del cuadro. c. Si se llega a una posicin ya ocupada (es decir, si arriba a la izquierda ya est ocupado con un nmero anterior), entonces la posicin a rellenar cambia, que ahora ser la inmediatamente debajo de la ltima casilla rellenada. Despus se contina el proceso tal y como se ha descrito en el punto anterior. Escriba un programa que genere el cuadro mgico de la dimensin que el usuario desee, y lo muestre luego por pantalla.

Este ejercicio ya ha sido resuelto en el captulo 19 (cfr. Ejercicio 10.7.) No vamos pues a comentarlo. Ahora se resuelve con memoria dinmica. La palabra clave typedef queda explicada en el Captulo 19.

Simplemente es una herramienta cmoda porque genera nuevos nombres para hacer referencia a tipos de dato, ya sean stos primitivos o creados por el programador. Aqu se usa simplemente porque es ms cmodo hacer referencia, por ejemplo, al tipo de dato uli, cuando queremos referirnos al unsigned long int.

/* ===== PROGRAMA DEL CUADRO MGICO.====================== */ #include <stdio.h> #include <stdlib.h> typedef unsigned long int uli;

537

Informtica Aplicada. Programacin en Lenguaje C.

typedef unsigned short int usi; typedef signed long int sli; typedef signed short int ssi; usi Dimension(void); void CuadroACero(uli**,usi); uli** AsignarMemoria(uli**,usi); void CrearCuadro(uli**,usi); void MostrarCuadro(uli**, usi); int main(void) { usi dim; uli **cuadro; usi i; do { // Valor de la dimensin ... dim = Dimension(); if(!dim) break; // Asignacin de memoria ... cuadro = AsignarMemoria(cuadro,dim); if(cuadro == NULL) break; // Inicializamos la matriz a cero... CuadroACero(cuadro,dim); // Asignar valores a los elementos del cuadro mgico... CrearCuadro(cuadro,dim); // Mostrar el cuadro mgico... MostrarCuadro(cuadro,dim); printf("\n\nPulse una tecla para mostrar otro cuadro ... "); getchar(); // Liberar la memoria reservada for(i = 0 ; i < dim ; i++) free(*(cuadro + i)); free(cuadro); }while(dim); return 0; } /* ------------------------------------------------------- */ /* Funcin Dimension() */ /* ------------------------------------------------------- */

538

Captulo 18. Asignacin dinmica de memoria.

usi Dimension(void) { usi d; do { clrscr(); printf("Dimension del cuadro. Debe ser un valor IMPAR ... "); printf("\nIndique dimension CERO si desea terminar la aplicacion -> "); scanf( %hu",&d); }while(!(d % 2) && d); return d; } /* ------------------------------------------------------- */ /* Funcin CuadroACero() */ /* ------------------------------------------------------- */ void CuadroACero(uli**C,usi d) { usi i, j; for(i = 0 ; i < d ; i++) for(j = 0 ; j < d ; j++) *(*(C + i) + j) = 0; } /* ------------------------------------------------------- */ /* Funcin AsignarMemoria() */ /* ------------------------------------------------------- */ uli** AsignarMemoria(uli**C,usi d) { usi i; if((C = (uli**)malloc(d * sizeof(uli*))) == NULL) { printf("\nError (1) de asignacion de memoria."); printf("\nLa ejecucion del programa no puede continuar."); printf("\nPulse cualquier tecla para terminar la aplicacion"); getchar(); return C; } for(i = 0 ; i < d ; i++) if((*(C + i)=(uli*)malloc(d*sizeof(uli)))==NULL) { printf("\nError (2) de asignacin de

539

Informtica Aplicada. Programacin en Lenguaje C.

memoria."); printf("\nLa ejecucin del programa no puede continuar."); printf("\nPulse cualquier tecla para terminar la aplicacion"); getchar(); C = NULL; } return(C); } /* ------------------------------------------------------- */ /* Funcin CrearCuadro() */ /* ------------------------------------------------------- */ void CrearCuadro(uli**C,usi d) { usi posX, posY, antX, antY, elem; // Posicin inicial: el centro de la primera fila... posX = d / 2; posY = 0; elem = 1; while(elem <= d * d) { *(*(C + posX) + posY) = elem; // Nueva posicin X ... antX = posX; posX = posX ? posX - 1 : d - 1; // Nueva posicion Y ... antY = posY; posY = posY ? posY - 1 : d - 1; // Si la casilla ya ha sido ocupada ... if(*(*(C + posX) + posY)) { posX = antX; posY = antY == d - 1 ? 0 : antY + 1; } elem++; } } /* ------------------------------------------------------- */ /* Funcin MostrarCuadro() */ /* ------------------------------------------------------- */ void MostrarCuadro(uli**C, usi d) { uli *sumaf,*sumac,sumad[2];

540

Captulo 18. Asignacin dinmica de memoria.

usi i, j; sumac = (uli*)malloc(d * sizeof(uli)); sumaf = (uli*)malloc(d * sizeof(uli)); if(sumaf != NULL) { for(i = 0 ; i < d ; i++) { *(sumaf + i) = 0; for(j = 0 ; j < d ; j++) *(sumaf + i) += *(*(C + i) + j); } } if(sumac != NULL) { for(i = 0 ; i < d ; i++) { *(sumac + i) = 0; for(j = 0 ; j < d ; j++) *(sumac + i) += *(*(C + j) + i); } } sumad[0] = sumad[1] = 0; for(i = 0 ; i < d ; i++) sumad[0] += *(*(C + i) + i); for(i = 1 ; i <= d ; i++) sumad[1] += *(*(C + d - i) + i - 1); for(i = 0 ; i < d ; i++) { printf("\n"); for(j = 0 ; j < d ; j++) printf("%5hu",*(*(C + j) + i)); printf(" -> %lu\n",*(sumaf + i)); } printf("\n"); for(i = 0 ; i < d ; i++) printf(" |"); printf("\n"); for(i = 0 ; i < d ; i++) printf(" V"); printf("\n\n"); for(i = 0 ; i < d ; i++) printf("%5lu",*(sumac + i)); printf("\n\nSuma Diagonal principal .... %5lu", sumad[0]);

541

Informtica Aplicada. Programacin en Lenguaje C.

printf("\n\nSuma Diagonal secundaria ... %5lu", sumad[1]); } Hemos implementado la funcin mostrar cuadre de manera que imprime tambin la verificacin de que todas las sumas de lneas y columnas y diagonales principales suman la misma cantidad.

542

CAPTULO 19
ALGUNOS USOS CON FUNCIONES.
En varios captulos anteriores (14, 15 y 17) hemos visto lo bsico y ms fundamental sobre funciones. Con todo lo dicho all se puede trabajar perfectamente en C, e implementar multitud de programas, con buena modularidad. En este nuevo captulo queremos presentar muy brevemente algunos usos ms avanzados de las funciones: distintas maneras en que pueden ser invocadas. Punteros a funciones, vectores de punteros a funciones, el modo de pasar una funcin como argumento de otra funcin. Son modos de hacer sencillos, que aaden, a todo lo dicho hasta el momento, posibilidades de diseo de programas. Otra cuestin que abordaremos en este captulo es cmo definir aquellas funciones de las que desconozcamos a priori el nmero de parmetros que han de recibir. De hecho, nosotros ya conocemos algunas de esas funciones: la funcin printf puede ser invocada con un solo parmetro (la cadena de caracteres que no imprima ningn valor) o con tantos

Informtica Aplicada. Programacin en Lenguaje C.

como se quiera: tantos como valores queramos que se impriman en nuestra cadena de caracteres. Veremos tambin aqu la manera de definir funciones con estas caractersticas.

Funciones de escape.
Existen ocasiones en que lo mejor que se puede hacer es abortar a ejecucin de una aplicacin antes de llegar a consecuencias ms desastrosas si se continuara la ejecucin del programa. A veces ms vale abortar misin intentando salvar la mayor parte de los muebles, que dejar que una situacin irresoluble arruine la lnea de ejecucin y entonces se produzca la interrupcin de la ejecucin de una forma no controlada por nosotros. Para realizar esas operaciones de salida inmediata disponemos de dos funciones, definidas en la biblioteca stdlib.h: la funcin exit y la funcin abort. La funcin exit nos permite abandonar la ejecucin de un programa antes de su finalizacin, volviendo el control al sistema operativo. Antes de regresar ese control, realiza algunas tareas importantes: por ejemplo, si el programa tiene abiertos algunos archivos, la funcin los cierra antes de abandonar la ejecucin de la aplicacin, y as esos archivos no se corrompen. Esta funcin se utiliza cuando no se cumple una condicin, imprescindible para la correcta ejecucin del programa. El prototipo de la funcin es void exit(int status); El parmetro status indica el modo en que se debe realizar la operacin de finalizacin inmediata del programa. El valor habitual es el cero, e indica que se debe realizar una salida inmediata llamada normal.

544

Captulo 19. Algunos usos con funciones.

Ahora mismo no vamos a poner ejemplos de uso de esta funcin. Pero ms adelante, en el prximo tema, se vern ocasiones claras en que su uso es muy necesario. La funcin abort es semejante. Su prototipo es: void abort(void) Y nos permite abandonar de forma anormal la ejecucin del programa, antes de su finalizacin. Escribe el mensaje Abnormal program termination y devuelve el control al sistema operativo.

Punteros a funciones.
En los primeros temas de este manual hablbamos de que toda la informacin de un ordenador se guarda en memoria. No slo los datos. Tambin las instrucciones tienen su espacio de memoria donde se almacenan y pueden ser ledas. Todo programa debe ser cargado sobre la memoria principal del ordenador antes de comenzar su ejecucin. Y si una funcin cualquiera tiene una ubicacin en la memoria, entonces podemos hablar de la direccin de memoria de esa funcin. Desde luego, una funcin ocupar ms de un byte, pero se puede tomar como direccin de memoria de una funcin aquella donde se encuentra la entrada de esa funcin. Y si tengo definida la direccin de una funcin No podr definir un puntero que almacene esa direccin? La respuesta es que s, y ahora veremos cmo poder hacerlo. Por tanto, podremos usar un puntero para ejecutar una funcin. Ese puntero ser el que tambin nos ha de permitir poder pasar una funcin como argumento de otra funcin. La declaracin de un puntero a una funcin es la declaracin de una variable. sta puede ser local, y de hecho, como siempre, ser lo habitual. Cuando se declara un puntero a funcin para poder asignarle posteriormente la direccin de una o u otra funcin, la declaracin de

545

Informtica Aplicada. Programacin en Lenguaje C.

ese puntero a funcin debe tener un prototipo coincidente con las funciones a las que se desea apuntar. Supongamos que tenemos las siguientes funciones declaradas al inicio de un programa: tipo_funcion nombre_funcion_1 (tipo1, , tipoN); tipo_funcion nombre_funcion_2 (tipo1, , tipoN); Y supongamos ahora que queremos declarar, por ejemplo en la funcin principal, un puntero que pueda recoger la direccin de estas dos funciones. La declaracin del puntero ser la siguiente: tipo_funcion (*puntero_a_funcion)(tipo1,,tipoN); De esta declaracin podemos hacer las siguientes importantes

observaciones: 1. tipo_funcion debe coincidir con el tipo de la funcin a la que va a apuntar el puntero a funcin. De la misma manera la lista de argumentos debe ser coincidente, tanto en los tipos de dato que intervienen como en el orden. En definitiva, los prototipos de la funcin y de puntero deber ser idnticos. 2. Si *puntero_a_funcion NO viniese recogido entre parntesis

entonces no estaramos declarando un puntero a funcin, sino una funcin normal que devuelve un tipo de dato puntero: un puntero para una recoger la direccin de una variable de tipo tipo_funcin. Por eso los parntesis no son opcionales. Una vez tenemos declarado el puntero, el siguiente paso ser siempre asignarle una direccin de memoria. En ese caso, la direccin de una funcin. La sintaxis para esta asignacin es la siguiente: puntero_a_funcion = nombre_funcion_1; Donde nombre_funcion_1 puede ser el nombre de cualquier funcin cuyo prototipo coincide con el del puntero.

546

Captulo 19. Algunos usos con funciones.

Una observacin importante: al hacer la asignacin de la direccin de la funcin, hacemos uso del identificador de la funcin: no se emplea el operador &; tampoco se ponen los parntesis al final del identificador de la funcin. Al ejecutar puntero_a_funcion obtendremos un comportamiento idntico al que tendramos si ejecutramos directamente la funcin. La sintaxis para invocar a la funcin desde el puntero es la siguiente: resultado = (*puntero_a_funcion)(var_1, ,var_N); Y as, cuando en la funcin principal se escriba esta sentencia tendremos el mismo resultado que si se hubiera consignado la sentencia resultado = nombre_a_funcion_1(var_1, ,var_N); Antes de ver algunos ejemplos, hacemos una ltima observacin. El puntero funcin es una variable local en una funcin. Mientras estemos en el mbito de esa funcin podremos hacer uso de ese puntero. Desde luego toda funcin trasciende el mbito de cualquier otra funcin; pero no ocurre as con los punteros. Veamos algn ejemplo. Hacemos un programa que solicita al usuario dos operandos y luego si desea sumarlos, restarlos, multiplicarlos o dividirlos. Entonces muestra el resultado de la operacin. Se definen cuatro funciones, para cada una de las cuatro posibles operaciones a realizar. Y un puntero a funcin al que se le asignar la direccin de la funcin que ha de realizar esa operacin seleccionada. El cdigo podra quedar como sigue: #include <stdio.h> float float float float sum(float, res(float, pro(float, div(float, float); float); float); float);

int main(void) { float a, b; unsigned char op;

547

Informtica Aplicada. Programacin en Lenguaje C.

float (*operacion)(float, float); printf("Primer operador ... "); scanf( %f",&a); printf("Segundo operador ... "); scanf( %f",&b); printf("Operacin ( + , - , * , / ) ... "); do op = getchar(); while(op !='+' && op !='-' && op !='*' && op !='/'); switch(op) { case '+': case '-': case '*': case '/': }

operacion operacion operacion operacion

= = = =

sum; res; pro; div;

break; break; break;

printf("\n%f %c %f = %f",a, op, b, (*operacion)(a, b)); return 0; } float sum(float x, float y) { return x + y; } float res(float x, float y) { return x - y; } float pro(float x, float y) { return x * y; } float div(float x, float y) { return y ? x / y : 0; } La definicin de las cuatro funciones no requiere a estas alturas explicacin alguna. El puntero operacin queda definido como variable local dentro de main. Dependiendo del valor de la variable op al puntero se le asignar la direccin de una de las cuatro funciones, todas ellos con idntico prototipo, igual a su vez al prototipo del puntero. Evidentemente, esto es slo un ejemplo. Hay otras muchas formas de resolver el problema, y quiz alguno piense que es ms complicado el uso del puntero, y que podra hacerse recogido en cada case de la estructura switch la llamada a la funcin correspondiente. Y no le

548

Captulo 19. Algunos usos con funciones.

faltar razn. Ya hemos dicho muchas veces que aqu tan hay tantas soluciones claras. vlidas como programadores. Pero desde luego las posibilidades de implementacin que ofrece el puntero a funcin son

Vectores de punteros a funciones.


No aportamos aqu ningn concepto nuevo, sino una reflexin sobre otra posibilidad que ofrece el tener punteros a funciones. Como todo puntero, un puntero a funcin puede formar parte de un array. Y como podemos definir arrays de todos los tipos que queramos, entonces podemos definir un array de tipo de dato punteros a funciones. Todos ellos sern del mismo tipo, y por tanto del mismo prototipo de funcin. La sintaxis de definicin ser la siguiente: tipo_funcion (*puntero_a_funcion[dimensin])(tipo1, tipoN); Y la asignacin puede hacerse directamente en la creacin del puntero, o en cualquier otro momento: tipo_funcion (*puntero_a_funcion[n])(tipo1, tipoN) = { funcion_1, funcin_2, , funcion_n } Donde deber haber tantos nombres de funcin, todas ellas del mismo tipo, como indique la dimensin del vector. Como siempre, cada una de las funciones deber quedar declarada y definida en el programa. El vector de funciones se emplea de forma anloga a cualquier otro vector. Se puede acceder a cada una de esas funciones mediante ndices, o por operatoria de punteros. Podemos continuar con el ejemplo del epgrafe anterior. Supongamos que la declaracin del puntero queda transformada en la declaracin de una array de dimensin 4: float(*operacion[4])(float,float)= {sum,res,pro,div};

549

Informtica Aplicada. Programacin en Lenguaje C.

Con esto hemos declarado cuatro punteros, cada uno de ellos apuntando a cada una de las cuatro funciones definidas. A partir de ahora ser lo mismo invocar a la funcin sumaf que invocar a la funcin apuntada por el primer puntero del vector. Si incorporamos en la funcin main la declaracin de una variable i de tipo entero, la estructura switch puede quedar ahora como sigue switch(op) { case '+': case '-': case '*': case '/': }

i i i i

= = = =

0; 1; 2; 3;

break; break; break;

Y ahora la ejecucin de la funcin ser como sigue: printf(\n\n%f %c %f = %f, a, op, b, (*operacin[i])(a, b));

Funciones como argumentos.


Se trata ahora de ver cmo hemos de definir un prototipo de funcin para que pueda recibir a otras funciones como parmetros. Un programa que usa funciones como argumentos suele ser difcil de comprender y de depurar, pero se adquiere a cambio una gran potencia en las posibilidades de C. La utilidad de pasar funciones como parmetro en la llamada a otra funcin est en que se puede hacer depender cul sea la funcin a ejecutar del estado a que se haya llegado a lo largo de la ejecucin. Estado que no puede prever el programador, porque depender de cada ejecucin concreta. Y as, una funcin que recibe como parmetro la direccin de una funcin, tendr un comportamiento u otro segn reciba la direccin de una u otra de las funciones declaradas y definidas. La sintaxis del prototipo de una funcin que recibe como parmetro la direccin de otra funcin es la habitual: primero el tipo de la funcin, seguido de su nombre y luego, entre parntesis, la lista de parmetros.

550

Captulo 19. Algunos usos con funciones.

Y entre esos parmetros uno o algunos pueden ser punteros a funciones. La forma en que se indica ese parmetro en la lista de parmetros es la siguiente: tipo_funcion (*puntero_a_funcion)(parmetros) (Lo que queda aqu recogido no es el prototipo de la funcin, sino el modo en que se consigna el parmetro puntero a funcin dentro de una lista de parmetros en un prototipo de funcin que recibe, entre sus argumentos, la direccin de una funcin.) Supongamos que este parmetro pertenece al prototipo de la funcin nombre_funcion. Entonces cuando se compile nombre_funcion el compilador slo sabr que esta funcin recibir como argumento, entre otras cosas, la direccin de una funcin que se ajusta al prototipo declarado como parmetro. Cul sea esa funcin es cuestin que no se conocer hasta el momento de la ejecucin y de la invocacin a esa funcin. La forma en que se llamar a la funcin ser la lgica de acuerdo con estos parmetros. El nombre de la funcin y seguidamente, entre parntesis, todos sus parmetros en el orden correcto. En el momento de recoger el argumento de la direccin de la funcin se har de la siguiente forma: *puntero_a_funcion(parmetros) De nuevo ser conveniente seguir con el ejemplo anterior, utilizando ahora una quinta funcin para realizar la operacin y mostrar por pantalla su resultado: #include <stdio.h> #include <conio.h> float sum(float, float); float res(float, float); float pro(float, float); float div(float, float); void mostrar(float, char, float, float(*f)(float, float));

551

Informtica Aplicada. Programacin en Lenguaje C.

int main(void) { float a, b; unsigned char op; float (*operacion[4])(float, float) ={sum,res,pro,div}; do { printf("\n\nPrimer operador ... "); scanf( %f",&a); printf("Segundo operador ... "); scanf( %f",&b); printf("Operacin ... \n)"); printf("\n\n1. Suma\n2. Resta); printf(\n3. Producto\n4. Cociente"); printf("\n\n\tSu opcin (1 , 2 , 3 , 4) ... "); do op = getche(); while(op - '0' < 1 || op - '0' > 4 ); mostrar(a,op,b,operacion[(short)(op - '1')]); printf("\n\nOtra operacin (s / n) ... "); do op = getche(); while(op != 's' && op != 'n'); }while(op == 's'); return 0; } float sum(float x, float y) { return x + y; } float res(float x, float y) { return x - y; } float pro(float x, float y) { return x * y; } float div(float x, float y) { return y ? x / y : 0; } void mostrar(float x,char c,float y,float(*f)(float,float)) { if(c == '1') c = '+'; else if(c == '2') c = '-'; else if(c == '3') c = '*'; else c = '/'; printf("\n\n%f %c %f = ", x, c, y); printf("%f.", (*f)(x,y)); }

552

Captulo 19. Algunos usos con funciones.

Vamos viendo poco a poco el cdigo. Primero aparecen las declaraciones de cinco funciones: las encargadas de realizar suma, resta, producto y cociente de dos valores float. Y luego, una quinta funcin, que hemos llamado mostrar, que tiene como cuarto parmetro un puntero a funcin. La declaracin de este parmetro es como se dijo: el tipo del puntero de funcin, el nombre del puntero, recogido entre parntesis y precedido de un asterisco, y luego, tambin entre parntesis, la lista de parmetros del puntero a funcin. As ha quedado declarada. Y luego comienza la funcin principal, main, donde viene declarado un vector de cuatro punteros a funcin. A cada uno de ellos le hemos asignado una de las cuatro funciones. Y hemos recogido el cdigo de toda la funcin main en un bloque do while, para que se realicen tantas operaciones como se deseen. Cada vez que se indique una operacin se har una llamada a la funcin mostrar que recibir como parmetro una de las cuatro direcciones de memoria de las otras cuatro funciones. La llamada es de la forma: mostrar(a,op,b,operacion[(short)(op - '1')]); Donde el cuarto parmetro es la direccin de la operacin

correspondiente. operacion[0] es la funcin sum; operacion[1] es la funcin res; operacion[2] es la funcin pro; y operacion[3] es la funcin div. El valor de op 1 ser 0 si op es el carcter 1; ser 1 si es el carcter 2; ser 2 si es el carcter 3; y ser 3 si es el carcter 4. Y ya estamos en la funcin mostrar, que simplemente tiene que ejecutar el puntero a funcin y mostrar el resultado por pantalla.

Ejemplo: la funcin qsort.


Hay ejemplos de uso de funciones pasadas como parmetros muy utilizados, como por ejemplo la funcin qsort, de la biblioteca stdlib.h. Esta funcin es muy eficaz en la ordenacin de grandes cantidades de valores. Su prototipo es:

553

Informtica Aplicada. Programacin en Lenguaje C.

void qsort(void *base, size_t nelem, size_t width, int (*fcmp)(const void*, const void*)); Es una funcin que no devuelve valor alguno. Recibe como parmetros el puntero base que es quien recoge la direccin del array donde estn los elementos a ordenar; nelem, que es un valor entero que indica la dimensin del vector pasado como primer parmetro; width es el tercer parmetro, que indica el tamao que tiene cada uno de los elementos del array; y por fin, el cuarto parmetro, es una funcin que devuelve un valor entero y que recibe como parmetros dos direcciones de dos variables. La funcin que se pase como parmetro en este puntero debe devolver un 1 si su primer parmetro apunta a un valor mayor que el segundo parmetro; el valor -1 si es al contrario; el valor 0 si el valor de ambos parmetros son iguales. Hay que explicar porqu los tipos que recoge el prototipo son siempre void. El motivo es porque la funcin qsort est definida para ser capaz de ordenar un array de cualquier tipo. Puede ordenar enteros, reales, letras, u otros tipos de dato mucho ms complejos, que se pueden crear y que veremos en un captulo posterior. La funcin no tiene en cuenta el tipo de dato: simplemente quiere saber dos cosas: 1. El tamao del tipo de dato; y eso se le facilita a la funcin a travs del tercer parmetro, width. 2. Cmo se define la ordenacin: como a priori no se sabe el tipo de dato, tampoco puede saber la funcin qsort con qu criterio decidir qu valores del dominio del tipo de dato son mayores, o iguales, o menores. Por eso, la funcin qsort requiere que el usuario le facilite, mediante una funcin muy simple que debe implementar cada usuario de la funcin qsort, ese criterio de ordenacin. Actualmente el algoritmo que da soporte a la funcin qsort es el ms eficaz en las tcnicas de ordenacin de grandes cantidades de valores.

554

Captulo 19. Algunos usos con funciones.

Vamos a ver un ejemplo de uso de esta funcin. Vamos a hacer un programa que ordene un vector bastante extenso de valores enteros que asignaremos de forma aleatoria. Para ello deberemos emplear tambin alguna funcin de generacin de aleatorios. Pero esa es cuestin muy sencilla que aclaramos antes de mostrar el cdigo de la funcin que hemos sugerido. Existe una funcin en stdlib.h llamada random. Esa funcin pretende ser un generador de nmeros aleatorios. Es un generador bastante malo, pero para nuestros propsitos sirve. Su prototipo es: int random(int num); Es una funcin que devuelve un entero aleatorio entre 0 y (num 1) . El valor de num que se le pasa a la funcin como parmetro tambin debe ser un valor entero. Cuando en una funcin se hace uso de la funcin random, antes debe ejecutarse otra funcin previa: la funcin randomize. Esta funcin inicializa el generador de aleatorios con un valor inicial tambin aleatorio. Su prototipo es: void randomize(void); Y se ejecuta en cualquier momento del programa, pero siempre antes de la primera vez que se ejecute la funcin random. Una vez presentadas las funciones necesarias para general aleatorios, veamos como queda un posible programa que genera una serie de enteros aleatorios y que los ordena de menor a mayor, haciendo uso de la funcin qsort: #include <stdio.h> #include <stdlib.h> #define TAM 10 #define RANGO 1000 int ordenar(void*,void*); int main(void) {

555

Informtica Aplicada. Programacin en Lenguaje C.

long numeros[TAM]; long i; randomize(); for(i = 0 ; i < TAM ; i++) numeros[i] = random(RANGO); // Vamos a ordenar esos numeros ... qsort((void*)numeros, TAM, sizeof(long), ordenar); // Mostramos resultados for(i = 0 ; i < TAM ; i++) printf("numeros[%4ld] = %ld\n", i, numeros[i]); return 0; } // La funcin de ordenacin ... int ordenar(void *a, void *b) { if(*(long*)a > *(long*)b) return 1; else if(*(long*)a < *(long*)b) return -1; return 0; } Hemos definido la funcin ordenar con un prototipo idntico al exigido por la funcin qsort. Recibe dos direcciones de memoria (nosotros queremos que sea de enteros largos, pero eso no se le puede decir a qsort) y resuelve cmo discernir la relacin mayor que, menor que, o identidad entre dos cualesquiera de esos valores que la funcin recibir como parmetros. La funcin trata a las dos direcciones de memoria como de tipo de dato void. El puntero a void ni siquiera sabe qu cantidad de bytes ocupa la variable a la que apunta. Toma la direccin del byte primero de nuestra variable, y no hace ms. Dentro de la funcin, el cdigo ya especifica, mediante el operador forzar tipo, que la variable apuntada por esos punteros ser tratada como una variable long. Es dentro de nuestra funcin donde especificamos el tipo de dato de los elementos que vamos a ordenar. Pero la funcin qsort van a poder usarla todos aquellos que

556

Captulo 19. Algunos usos con funciones.

tengan algo que ordenar, independientemente de qu sea ese algo: porque todo el que haga uso de qsort le explicar a esa funcin, gracias al puntero a funciones que recibe como parmetro, el modo en que se decide quin va antes y quien va despus. Lo que aporta qsort es la rapidez en poner en orden una cantidad ingente de valores del mismo tipo. Y, efectivamente, hay muchas formas de resolver los problemas y de implementarlos. Y el uso de punteros a funciones, o la posibilidad de pasar como parmetro de una funcin la direccin de memoria de otra funcin es una posibilidad que ofrece enormes ventajas y posibilidades.

Estudio de tiempos.
A veces es muy ilustrativo poder estudiar la velocidad de algunas aplicaciones que hayamos implementado en C. En algunos programas de ejemplo de captulos anteriores habamos presentado un programa que ordenaba cadenas de enteros. Aquel programa, que ahora mostraremos de nuevo, estaba basado en un mtodo de ordenacin llamado mtodo de la burbuja: consiste en ir pasando para arriba aquellos enteros menores, de forma que van quedando cada vez ms abajo, o ms atrs (segn se quiera) los enteros mayores. Por eso se llama el mtodo de la burbuja: porque lo liviano sube. Vamos a introducir una funcin que controla el tiempo de ejecucin. Hay funciones bastante diversas para este estudio. Nosotros nos vamos ahora a centrar en una funcin, disponible en la biblioteca time.h, llamada clock, cuyo prototipo es: clock_t clock(void); Vamos a considerar por ahora que el tipo de dato clock_t es equivalente a tipo de dato long (de hecho as es). Esta funcin est

557

Informtica Aplicada. Programacin en Lenguaje C.

recomendada para medir intervalos de tiempo. El valor que devuelve es proporcional al tiempo trascurrido desde el inicio de ejecucin del programa en la que se encuentra esa funcin. Ese valor devuelto ser mayor cuanto ms tarde se ejecute esta funcin clock, que no realiza tarea alguna ms que devolver el valor actualizado del contador de tiempo. Cada breve intervalo de tiempo (bastantes veces por segundo: no vamos ahora a explicar este aspecto de la funcin) ese contador que indica el intervalo de tiempo transcurrido desde el inicio de la ejecucin del programa, se incrementa en uno. Un modo de estudiar el tiempo trascurrido en un proceso ser el siguiente: time_t t1, t2; t1 = clock(); (proceso a estudiar su tiempo) t2 = clock(); printf(Intervalo transcurrido: %ld, t2 t1); El valor que imprimir este cdigo ser proporcional al tiempo invertido en la ejecucin del proceso del que estudiamos su ejecucin. Si esa ejecucin es muy rpida posiblemente el resultado sea cero. Veamos ahora dos programas de ordenacin. El primero mediante la tcnica de la burbuja. Como se ve, en esta ocasin trabajamos con un vector de cien mil valores para ordenar: #include <stdio.h> #include <stdlib.h> #include <time.h> #define TAM 100000 #define RANGO 10000 int cambiar(long*, long*); int main(void) { long numeros[TAM]; long i, j; time_t t1, t2; randomize(); for(i = 0 ; i < TAM ; i++) numeros[i] = random(RANGO);

558

Captulo 19. Algunos usos con funciones.

// Vamos a ordenar esos numeros ... // Mtodo de la burbuja ... t1 = clock(); for( i = 0 ; i < TAM ; i++) for(j = i ; j < TAM ; j++) if(numeros[i] > numeros[j]) cambiar(numeros + i, numeros + j); t2 = clock(); printf("t2 - t1 = %ld.\n", t2 - t1); return 0; } int cambiar(long *a, long *b) { *a ^= *b; *b ^= *a; *a ^= *b; } Si lo ejecuta en su ordenador, le aparecer por pantalla (quiz tarde unos segundos: depende de lo rpido que sea su ordenador) una nmero. Si es cero, porque la ordenacin haya resultado muy rpida, simplemente aumente el valor de TAM y vuelva a compilar y ejecutar el programa. Ahora escriba este otro programa, que ordena mediante la funcin qsort. Es el que hemos visto antes, algo modificado para hacer la comparacin de tiempos: #include <stdio.h> #include <stdlib.h> #include <time.h> #define TAM 100000 #define RANGO 10000 int ordenar(void*,void*); int main(void) { long numeros[TAM]; long i, j; time_t t1, t2; randomize(); for(i = 0 ; i < TAM ; i++) numeros[i] = random(RANGO); // Vamos a ordenar esos numeros ... // Mediante la funcin qsort ...

559

Informtica Aplicada. Programacin en Lenguaje C.

t1 = clock(); qsort((void*)numeros, TAM, sizeof(long), ordenar); t2 = clock(); printf("t2 - t1 = %ld.", t2 - t1); return 0; } int ordenar(void *a, void *b) { if(*(long*)a > * (long*)b) return 1; else if(*(long*)a < *(long*)b) return -1; return 0; } Si ejecuta ahora este programa, obtendr igualmente la ordenacin de los elementos del vector. Pero ahora el valor que saldr por pantalla es del orden de 500 veces ms bajo. El algoritmo de ordenacin de la burbuja es muy cmodo de

implementar, y es eficaz para la ordenacin de unos pocos centenares de enteros. Pero cuando hay que ordenar grandes cantidades, no es suficiente con que el procedimiento sea tericamente vlido: adems debe ser eficiente. Programar no es slo poder en un lenguaje una serie de instrucciones. Adems de saber lenguajes de programacin es conveniente conocer de qu algoritmos se disponen para la solucin de nuestros problemas. O echar mano de soluciones ya adoptadas, como en este caso, la implementacin de la funcin qsort.

Creacin de MACROS.
La directiva #define, que ya hemos visto, permite la definicin de macros. Una macro es un bloque de sentencias a las que se les ha asignado un nombre o identificador.

560

Captulo 19. Algunos usos con funciones.

Una macro no es una funcin. Es cdigo que se inserta all donde aparece su identificador. Veamos un ejemplo sencillo: #include <stdio.h> // Definicin de la macro ... #define cuadrado(x) x * x int main(void) { short a; unsigned long b; printf("Intoduzca el valor de a ... "); scanf( %hd",&a); printf("El cuadrado de %hd es %lu", a, cuadrado(a)); return 0; } cuadrado NO es una funcin, aunque su invocacin tenga la misma apariencia. En el cdigo no aparece ni un prototipo con ese nombre, ni su definicin. Es una macro: el cdigo x * x aparecer all donde en nuestro programa se ponga cuadrado(x). #define es una directiva del compilador. Antes de compilar, se busca en todo el texto todas las veces donde venga escrita la cadena cuadrado(expresin). Y en todas ellas sustituye esa cadena por la segunda parte de la directiva define: en este caso, lo sustituye por la cadena expresin * expresin. En nuestro ejemplo hemos calculado el cuadrado de la variable a; en general, se puede calcular el cuadrado de cualquier expresin. En definitiva una macro es un bloque de cdigo que se va a insertar, previamente a la compilacin, en todas aquellas partes de nuestro programa donde se encuentre su identificador. Una macro puede hacer uso de otra macro. Por ejemplo: #define cuadrado(x) x * x #define circulo(r) 3.141596 * cuadrado(r) La macro circulo calcula la superficie de una circunferencia de radio r. Para realizar el clculo, hace uso de la macro cuadrado, que calcula el

561

Informtica Aplicada. Programacin en Lenguaje C.

cuadrado del radio. La definicin de una macro debe preceder siempre a su uso. No se podra definir la macro circulo como se ha hecho si, previamente a su definicin, no estuviera recogida la definicin de la macro cuadrado. Las macros pueden llegar a ser muy extensas. Vamos a rehacer el cdigo del programa del tema anterior sobre la criba de Erastthenes, usando macros en lugar de funciones.

Ejemplo de MACRO: la Criba de Erastthenes.


El cdigo mediante funciones que resuelve la criba de Erastthenes ha quedado resuelto al final del captulo 17 (cfr. Ejercicio 17.1.). El propsito ahora es rehacer toda la aplicacin sin hacer uso de funciones: definiendo macros. Para poder ver la diferencia entre utilizar macros y utilizar funciones convendr presentar de nuevo las dos funciones que se haban definido para la aplicacin, y poder comparar cmo se rehacen mediante una directiva de procesador. Las dos funciones eran la funcin Criba y la funcin TablaPrimos. La primera era: long Criba(char* num, long rango) { long i, j; // En principio marcamos todos los elementos como PRIMOS for(i = 0 ; i < rango; i++) num[i] = 'p'; for(i = 2 ; i < sqrt(rango) ; i++) if(num[i] == 'p') for(j = 2 * i ; j < rango ; j += i) num[j] = 'c'; for( i = 1 , j = 0 ; i < rango ; i++) if(num[i] == 'p') j++; return j; } Ahora, con la macro, que hemos llamado __Criba, queda de la siguiente forma:

562

Captulo 19. Algunos usos con funciones.

#define __Criba(_num, _pr) { long _i, _j; for(_i = 0 ; _i < MAX; _i++) _num[_i] = 'p'; for(_i = 2 ; _i < sqrt(MAX) ; _i++) if(_num[_i] == 'p') for(_j = 2 * _i ; _j < MAX ; _j += _i) _num[_j] = 'c'; for(_i = 1 , _j = 0 ; _i < MAX ; _i++) if(_num[_i] == 'p') _j++; _pr = _j; }

\ \ \ \ \ \ \ \ \ \ \

Las barras invertidas al final de cada lnea indican que aunque hay un salto de lnea en el editor, el texto contina en la lnea siguiente. Deben ponerse tal cual, sin espacio en blanco alguno posteriormente a ellas. Las diferencias principales con respecto al cdigo de la funcin se basan en las siguientes peculiaridades de las macros: 1. El nombre: mucha gente habita a preceder al nombre de las macros uno o varios caracteres subrayado; nosotros hemos utilizado dos. Es cuestin de criterio personal, y es muy conveniente usar un criterio de creacin de identificadores especial para las macros. Si se decide que las macros comienzan con dos caracteres subrayado, y tenemos la disciplina de trabajo de no crear jams, en cdigo normal, un identificador con ese inicio, entonces es imposible que la macro pueda generar confusin. Dentro de las macros, es habitual tambin darle un formato especial a los nombres de las variables que se definan en ellas: en el ejemplo se han tomado todas las variables, y todos los parmetros de la macro con un carcter subrayado al principio. Es muy importante dar nombres especiales a esas variables: hay que tener en cuenta que el cdigo se inserta tal cual en la funcin que invoca a la macro: en nuestro ejemplo, si las variables de la macro se hubieran llamado i y j en lugar de _i y _j, tendramos un error de compilacin, porque esas variables, con el identificador i y con el identificador j ya estn creadas en la funcin principal que utiliza las macros.

563

Informtica Aplicada. Programacin en Lenguaje C.

2. Hay que considerar que la macro lo que hace es insertar el cdigo en el lugar donde se coloca el identificador: si necesitamos crear variables, habr que definir la macro como un bloque (comenzar y terminar con llaves) para no tener que arrastrar luego todas esas variables en el mbito de la funcin que llama a la macro. Si la funcin que convertimos en macro devolva un valor, ahora habr que ver la manera de que ese valor quede recogido al final de la macro: lo habitual ser que se pase como parmetro la variable donde iba a quedar almacenado el valor que devolva la funcin. En nuestro caso hemos pasado como parmetro la variable pr. La variable que en la funcin se llamaba rango ha quedado eliminada porque si trabajamos con macros podemos hacer uso de la que define el valor de MAX, cosa que evitbamos al redactar la funcin: no queramos que la funcin tuviera un valor dependiente de una macro definida en la aplicacin donde se defina la funcin, para permitir que la funcin fuese lo ms transportable posible, y no dependiente de un valor ajeno a su propia definicin. La segunda funcin era: void TablaPrimos(char* num, long* primos, long rango) { long i, j; for(i = 1 , j = 0 ; i < rango ; i++) if(num[i] == 'p') { *(primos + j) = i; j++; } *(primos + j) = 0; } Y ahora, con la macro, que hemos llamado __TablaPrimos, queda de la siguiente forma: #define __TablaPrimos(_num, _primos) { long _i, _j; for(_i = 1 , _j = 0 ; _i < MAX ; _i++) if(_num[_i] == 'p') { *(_primos + _j) = _i; \ \ \ \ \ \

564

Captulo 19. Algunos usos con funciones.

_j++;} *(_primos + _j) = 0; }

\ \

Donde de nuevo hemos eliminado el uso del tercer parmetro que se recoga en una variable llamada rango. Hemos mantenido el criterio para la asignacin de nombres. Hemos recibido como parmetros los dos punteros: en la macro se llaman con una carcter subrayado previo: cuando se sustituye el cdigo de la macro en el programa, antes de la compilacin, el nombre que se recoge es el que se haya consignado entre parntesis en la llamada de la macro: y ah los nombres van sin esos caracteres subrayado. Para un usuario que no haya definido la macro, el que un bloque de cdigo sea macro o sea funcin es algo que no ha de saber. El modo de invocacin es el mismo (cambiando los parmetros). Muchas de las funciones estndares de C hacen uso de macros; otras no son realmente tales, sino que son macros. La ventaja de la macro es que el cdigo queda insertado en la aplicacin antes de la compilacin, de forma que su uso no exige la llamada a una funcin, el apile en memoria de las variables que se deben guardar mientras salimos de un mbito para meternos en el mbito de la nueva funcin en ejecucin, etc. El uso de macros reduce los tiempos de ejecucin de las aplicaciones notablemente. Una macro consume, de media, un 20 % menos del tiempo total que tardara, en hacer lo mismo, el cdigo definido en forma de funcin.

Funciones con un nmero variable de argumentos.


Hasta el momento hemos visto funciones que tienen definido un nmero de parmetros concreto. Y son, por tanto, funciones que al ser invocadas se les debe pasar un nmero concreto y determinado de parmetros.

565

Informtica Aplicada. Programacin en Lenguaje C.

Sin embargo no todas las funciones que hemos utilizado son realmente as de rgidas. Por ejemplo, la funcin printf, tantas veces invocada en nuestros programas, no tiene un nmero prefijado de parmetros: printf(Aqu solo hay un parametro.); printf(Aqu hay %ld parmetros., 2); printf(Y ahora %ld%c, 3, .); // Un parmetro // Dos parmetros // Tres parmetros

Vamos a ver en este epgrafe cmo lograr definir una funcin en la que el nmero de parmetros sea variable, en funcin de las necesidades que tenga el usuario en cada momento. Existen una serie de macros que permiten definir una funcin como si tuviera una lista variable de parmetros. Esas macros, que ahora veremos, estn definidas en la biblioteca stdarg.h. El prototipo de las funciones con un nmero variable de parmetros es el siguiente: tipo nombre_funcion(tipo_1,[..., tipo_N], ...); Primero se recogen todos los parmetros de la funcin que son fijos, es decir, aquellos que siempre debern aparecer como parmetros en la llamada a la funcin. En el caso de la funcin printf, siempre debe aparecer, al principio, una cadena de caracteres, que viene recogida entre comillas. Si falta esta cadena en la funcin printf tendremos error en tiempo de compilacin. Y despus de los parmetros fijos y obligatorios (como veremos ms adelante, toda funcin que admita un nmero variable de parmetros, al menos deber tener un parmetro fijo) vienen tres puntos suspensivos. Esos puntos deben ir al final de la lista de argumentos conocidos, e indican que la funcin puede tener ms argumentos, de los que no sabemos ni cuntos ni de qu tipo de dato. La funcin que tiene un nmero indeterminado de parmetros, deber averiguar cules recibe en cada llamada. La lista de parmetros deber

566

Captulo 19. Algunos usos con funciones.

ser recogida por la funcin, que deber deducir de esa lista cules son los parmetros recibidos. Para almacenar y operar con esta lista de argumentos, est definido, en la biblioteca stdarg.h, un nuevo tipo de dato de C, llamado va_list (podramos decir que es el tipo de lista de argumentos). En esa biblioteca viene definido el tipo de dato y las tres macros empleadas para operar sobre objetos de tipo lista de argumentos. Este tipo de dato tendr una forma similar a una cadena de caracteres. Toda funcin con un nmero de argumentos variable deber tener declarada, en su cuerpo, una variable de tipo de dato va_list. tipo nombre_funcion (tipo_1,[..., tipo_N], ...) { va_list argumentos /* lista de argumentos */ Lo primero que habr que hacer con esta variable de tipo va_list ser inicializarla con la lista de argumentos variables recibida en la llamada a la funcin. Para inicializar esa variable se emplea una de las tres macros definidas en la biblioteca stdarg.h: la macro va_start, que tiene la siguiente sintaxis: void va_start(va_list ap, lastfix); Donde ap es la variable que hemos creado como de tipo de dato va_list, y donde lastfix es el ltimo argumento fijo de la lista de argumentos. La macro va_start asigna a la variable ap la direccin del primer argumento variable que ha recibido la funcin. Necesita, para esta operacin, recibir el nombre del ltimo parmetro fijo que recibe la funcin como argumento. Se guarda en la variable ap la direccin del comienzo de la lista de argumentos variables. Esto obliga a que en cualquier funcin con nmero de argumentos variable exista al menos un argumento de los llamados aqu fijos, con nombre en la definicin de

567

Informtica Aplicada. Programacin en Lenguaje C.

la funcin. En caso contrario, sera imposible obtener la direccin del comienzo para una lista de los restantes argumentos. Ya tenemos localizada la cadena de argumentos variables. Ahora ser necesario recorrerla para extraer todos los argumentos que ha recibido la funcin en su actual llamada. Para eso est definida una segunda macro de stdarg.h: la macro va_arg. Esta rutina o macro extrae el siguiente argumento de la lista. Su sintaxis es: tipo va_arg(va_list ap, tipo); Donde el primer argumento es, de nuevo, nuestra lista de argumentos, llamada ap, que ya ha quedado inicializada con la macro va_start. Y tipo es el tipo de dato del prximo parmetro que se espera encontrar. Esa informacin es necesaria: por eso, en la funcin printf, indicamos en el primer parmetro (la cadena que ha de ser impresa) los especificadores de formato. La rutina va extrayendo uno tras otro los argumentos de la lista variable de argumentos. Para cada nuevo argumento se invoca de nuevo a la macro. La macro extrae de la lista ap el siguiente parmetro (que ser del tipo indicado) y avanza el puntero al siguiente parmetro de la lista. La macro devuelve el valor extrado de la lista. Para extraer todos los elementos de la lista habr que invocar a la macro va_arg tantas veces como sea necesario. De alguna manera la funcin deber detectar que ha terminado ya de leer en la lista de variables. Por ejemplo, en la funcin printf, se invocar a la macro va_arg tantas veces como veces haya aparecido en la primera cadena un especificador de formato: un carcter % no precedido del carcter \. Si se ejecuta la macro va_arg menos veces que parmetros se hayan pasado en la actual invocacin, la ejecucin no sufre error alguno: simplemente dejarn de leerse esos argumentos. Si se ejecuta ms

568

Captulo 19. Algunos usos con funciones.

veces que parmetros variables se hayan pasado, entones el resultado puede ser imprevisible. Si, despus de la cadena de texto que se desea imprimir, la funcin printf recoge ms expresiones (argumentos en la llamada) que caracteres % ha consignado en la cadena (primer argumento de la llamada), no pasar percance alguno: simplemente habr argumentos que no se imprimirn y ni tan siquiera sern extrados de la lista de parmetros. Pero si hay ms caracteres % que variables e nuestra lista variable de argumentos, entonces la funcin printf ejecutar la macro va_arg en busca de argumentos no existentes. En ese caso, el resultado ser completamente imprevisible. Y cuando ya se haya recorrido completa la lista de argumentos, entonces deberemos ejecutar una tercera rutina que restablece la pila de llamada a funciones. Esta macro es necesaria para permitir la finalizacin correcta de la funcin y que pueda volver el control de programa a la sentencia inmediatamente posterior a la de la llamada de la funcin de argumentos variables. Su sintaxis es: void va_end(va_list ap); Veamos un ejemplo. Hagamos un programa que calcule la suma de una serie de variables double que se reciben. El primer parmetro de la funcin indicar cuntos valores intervienen en la suma; los dems parmetros sern esos valores. La funcin devolver la suma de todos ellos: #include <stdio.h> #include <stdarg.h> double sum(long, ...); int main(void) { double S; S = sum(7, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0); printf("%f",S);

569

Informtica Aplicada. Programacin en Lenguaje C.

return 0; } double sum(long v,...) { double suma = 0; long i; va_list sumandos; va_start(sumandos, v); for(i = 0 ; i < v ; i++) suma += va_arg(sumandos,double); va_end(sumandos); return suma; } La funcin sumaf recibe un nico parmetro fijo, que es el que indica cuntas variables ms va a recibir la funcin. As cuando alguien quiere sumar varios valores, lo que hace es invocar a la funcin sumaf indicndole en un primer parmetro el nmero de sumandos y, a continuacin, esos sumandos que se ha indicado. Despus de inicializar la variable sumandos de tipo va_list mediante la macro va_start, se van sumando todos los argumentos recibidos en la variable suma. Cada nuevo sumando se obtiene de la cadena sumandos gracias a una nueva invocacin de la macro va_arg. Tendr tantas sumas como indique el parmetro fijo recibido en la variable v. Al final, y antes de la sentencia return, ejecutamos la macro que restaura la pila de direcciones de memoria (va_end), de forma que al finalizar la ejecucin de la funcin el programa lograr transferir el control a la siguiente sentencia posterior a la que invoc la funcin de parmetros variables. Observacin: las funciones con parmetros variables presentan

dificultades cuando deben cargar, mediante la macro va_arg, valores de tipo char, unsigned char y valores float. Hay problemas de promocin de variables y los resultados no son esperados. finalmente los

570

Captulo 19. Algunos usos con funciones.

Argumentos de la lnea de rdenes.


Ya hemos explicado que en todo programa, la nica funcin ejecutable es la funcin principal: la funcin main. Un programa sin funcin principal puede ser compilable, pero no llegar a generar un programa ejecutable, porque no tiene la funcin de arranque. As, vemos que todas las funciones de C pueden definirse con parmetros: es decir, pueden ejecutarse con unos valores de arranque, que sern diferentes cada vez que esa funcin sea invocada. Tambin se puede hacer eso con la funcin main. En ese caso, quien debe pasar los parmetros de arranque a la funcin principal ser el usuario del programa compilado en el momento en que indique al sistema operativo el inicio de esa ejecucin de programa. En muchos sistemas operativos (UNIX especialmente) es posible, cuando se ejecuta un programa compilado de C, pasar parmetros a la funcin main. Esos parmetros se pasan en la lnea de comandos que lanza la ejecucin del programa. Para ello, en esa funcin principal se debe haber incluido los siguientes parmetros: tipo main(int argc, char *argv[] ) Donde argc recibe el nmero de argumentos de la lnea de comandos, y argv es un array de cadenas de caracteres donde se almacenan los argumentos de la lnea de comandos. Los usos ms comunes para los argumentos pasados a la funcin principal son el pasar valores para la impresin, el paso de opciones de programa (muy empleado eso en unix, o en DOS), el paso de nombres de archivos donde acceder a informacin en disco o donde guardar la informacin generada por el programa, etc. La funcin main habr recibido tantos argumentos como diga la variable argc. El primero de esos argumentos es siempre el nombre del programa. Los dems argumentos deben ser valores esperados, de

571

Informtica Aplicada. Programacin en Lenguaje C.

forma que la funcin principal sepa qu hacer con cada uno de ellos. Alguno de esos argumentos puede ser una cadena de control que indique la naturaleza de los dems parmetros, o al menos de alguno de ellos. Desde luego, los nombres de las variables argc y argv son mera convencin: cualquier identificador que se elija servir de la misma manera. Y un ltimo comentario. Hasta el momento, siempre que hemos definido la funcin main, la hemos declarado de tipo int. Realmente esta funcin puede ser de cualquier tipo, incluso de tipo void. Desde luego, la sentencia return debe devolver un valor del tipo de la funcin, o ninguno si hemos declarado a la funcin main como de tipo void. Veamos un ejemplo. Hacemos un programa que al ser invocado se le pueda facilitar una serie de datos personales, y los muestre por pantalla. El programa espera del usuario que introduzca su nombre, su profesin y/o su edad. Si el usuario quiere introducir el nombre, debe precederlo con la cadena -n; si quiere introducir la edad, deber precederla la cadena -e; y si quiere introducir la profesin, deber ir precedida de la cadena -p. #include <stdio.h> #include <string.h> int main(int argc, char*argv[]) { char nombre[30]; char edad[5]; char profesion[30]; nombre[0] = profesion[0] = edad[0] = '\0'; long i; for(i = 0 ; i < argc ; i++) { if(strcmp(argv[i],"-n") == 0) { i++; if(i < argc) strcpy(nombre, argv[i]); } else if(strcmp(argv[i],"-p") == 0) { i++;

572

Captulo 19. Algunos usos con funciones.

if(i < argc) strcpy(profesion, argv[i]); } else if(strcmp(argv[i],"-e") == 0) { i++; if(i < argc) strcpy(edad, argv[i]); } } printf("Nombre: %s\n", nombre); printf("Edad: %s\n", edad); printf("Profesion: %s\n", profesion); return 0; } Si ha entrado la cadena n, entonces la siguiente cadena deber ser el nombre: y as se almacena. Si se ha entrado la cadena p, entonces la siguiente cadena ser la profesin: y as se guarda. Y lo mismo ocurre con la cadena e y la edad. Al compilar el programa, quedar el ejecutable en algn directorio: en principio en el mismo en donde se haya guardado el documento .cpp. Si ejecutamos ese programa (supongamos que le hemos llamado datos) con la siguiente lnea de comando: datos p estudiante e 21 n Isabel Aparecer por pantalla la siguiente informacin: Nombre: Isabel Edad: 21 Profesion: estudiante

Ejercicios.

19.1.

Una vez ha creado un vector que contiene todos los primos menores que un lmite superior dado, declare y defina una funcin que busque los primos enlazados. Se llaman primos enlazados a aquellos primos cuya diferencia es igual a dos, es decir, que son dos impares consecutivos: por ejemplo 11 y 13;

573

Informtica Aplicada. Programacin en Lenguaje C.

17 y 19, etc. (Sin resolver)

19.2.

Lea el siguiente cdigo y muestre la salida que ofrecer por pantalla.

#include <stdio.h> #include <math.h> #define CUADRADO(x) x*x #define SUMA(x,y) CUADRADO(x) + CUADRADO(y) #define RECTO(x,y) sqrt(SUMA(x,y)) int main(void) { printf("%2.1f",RECTO(3,4)); return 0; }

574

CAPTULO 20
ESTRUCTURAS ESTTICAS DE DATOS Y DEFINICIN DE TIPOS.
En uno de los primeros captulos hablamos largamente de los tipos de dato. Decamos que un tipo de dato determina un dominio (conjunto de valores posibles) y unos operadores definidos sobre esos valores. Hasta el momento hemos trabajado con tipos de dato estndar en C. Pero con frecuencia hemos hecho referencia a que se pueden crear otros diversos tipos de dato, ms acordes con las necesidades reales de muchos problemas concretos que se abordan con la informtica. Hemos visto, de hecho, ya diferentes tipos de dato a los que por ahora no hemos prestado atencin alguna, pues an no habamos llegado a este captulo: tipo de dato size_t, time_t: cada vez que nos los hemos encontrado hemos despejado con la sugerencia de que se considerasen, sin ms, tipos de dato iguales a long. En este tema vamos a ver cmo se pueden definir nuevos tipos de dato.

Informtica Aplicada. Programacin en Lenguaje C.

Tipos de dato enumerados.


La enumeracin es el modo ms simple de crear un nuevo tipo de dato. Cuando definimos un tipo de dato enumerado lo que hacemos es definir de forma explcita cada uno de los valores que formarn parte del dominio de ese nuevo tipo de dato: es una definicin por extensin. La sintaxis de creacin de un nuevo tipo de dato enumerado es la siguiente: enum identificador {id_1[, id_2, ..., id_N]}; Donde enum es una de las 32 palabras reservadas de C. Donde identificador es el nombre que va a recibir el nuevo tipo de dato. Y donde id_1, etc. son los diferentes identificadores de cada uno de los valores del nuevo dominio creado con el nuevo tipo de dato. Mediante la palabra clave enum se logran crear tipos de dato que son subconjunto de los tipos de dato int. Los tipos de dato enumerados tienen como dominio un subconjunto del dominio de int. De hecho las variables creadas de tipo enum son tratadas, en todo momento, como si fuesen de tipo int. Lo que hace enum es mejorar la legibilidad del programa. Pero a la hora de operar con sus valores, se emplean todos los operadores definidos para int. Veamos un ejemplo: enum semaforo {verde, amarillo, rojo}; Al crear un tipo de dato as, acabamos de definir: 1. Un dominio: tres valores definidos, con los literales verde, amarillo y rojo. En realidad el ordenador los considera valores 0, 1 y 2. 2. Una ordenacin intrnseca a los valores del nuevo dominio: verde menor que amarillo, y amarillo menor que rojo.

576

Captulo 20. Estructuras estticas de datos y definicin de tipos.

Acabamos, pues, de definir un conjunto de valores (subconjunto de los enteros), con identificadores propios y nicos, que presenta la propiedad de ordenacin. Luego, en la funcin que sea necesario utilizar ese tipo de dato, se pueden declarar variables con la siguiente sintaxis: enum identificador nombre_variable; En el caso del tipo de dato semforo, podemos definir en una funcin una variable de la siguiente forma: enum semaforo cruce; Veamos otro ejemplo enum judo {blanco,amarillo,naranja,verde,azul,marron,negro}; int main(void) { enum judo c; printf(Los colores definidos son ... \n); for(c = blanco ; c <= negro ; c++) printf("%d\t",c); return 0; } La funcin principal mostrar por pantalla los valores de todos los colores definidos: el menor es el banco, y el mayor el negro. Por pantalla aparecer la siguiente salida: Los colores definidos son ... 0 1 2 3 4 5 6

Dar nombre a los tipos de dato.


A lo largo del presente captulo veremos la forma de crear diferentes estructuras de datos que den lugar a tipos de dato nuevos. Luego, a estos tops de dato se les puede asignar un identificador o un nombre para poder hacer referencia a ellos a la hora de crear nuevas variables.

577

Informtica Aplicada. Programacin en Lenguaje C.

La palabra clave typedef, de C, permite crear nuevos nombres para los tipos de dato creados. Una vez se ha creado un tipo de dato y se ha creado el nombre para hacer referencia a l, ya podemos usar ese identificador en la declaracin de variables, como si fuese un tipo de dato estndar en C. En sentido estricto, las sentencias typedef no crean nuevos tipos de dato: lo que hacen es asignar un identificador definitivo para esos tipos de dato. La sintaxis para esa creacin de identificadores es la siguiente: typedef tipo nombre_tipo; As, se pueden definir los tipos de dato estndar con otros nombres, que quiz convengan por la ubicacin en la que se va a hacer uso de esos valores definidos por el tipo de dato. Y as tenemos: typedef unsinged size_t; typedef long time_t; O podemos nosotros mismos reducir letras: typedef typedef typedef typedef unsigned long int uli; unsigned short int usi; signed short int ssi; signed long int sli;

O tambin: typedef char* CADENA; Y as, a partir de ahora, en todo nuestro programa, nos bastar declarar las variables enteras como uno de esos nuevos cuatro tipos. O declarar una cadena de caracteres como una variable de tipo CADENA. Es evidente que con eso no se ha creado un nuevo tipo de dato, sino simplemente un nuevo identificador para un tipo de dato ya existente. Tambin se puede dar nombre a los nuevos tipos de dato creados. En el ejemplo del tipo de dato enum llamado semaforo, podramos hacer: typedef enum {verde, amarillo, rojo} semaforo;

578

Captulo 20. Estructuras estticas de datos y definicin de tipos.

Y as, el identificador semforo quedara definitivamente como nuevo tipo de dato. Luego, en una funcin que necesitase un tipo de dato de este tipo, ya no diramos: enum semaforo cruce; Sino simplemente semaforo cruce; Ya que el identificador semaforo ya ha quedado como identificador vlido de tipo de dato en C.

Estructuras de datos y tipos de dato estructurados.


Comenzamos ahora a tratar de la creacin de verdaderos nuevos tipos de dato. En C, adems de los tipos de dato primitivos, se pueden utilizar otros tipos de dato definidos por el usuario. Son tipos de dato que llamamos estructurados, que se construyen mediante componentes de tipos ms simples previamente definidos o tipos de dato primitivos, que se denominan elementos de tipo constituyente. Las propiedades que definen un tipo de dato estructurado son el nmero de componentes que lo forman (que llamaremos cardinalidad), el tipo de dato de los componentes y el modo de referenciar a cada uno de ellos. Un ejemplo de tipo de dato estructurado ya lo hemos definido y utilizado de hecho: las matrices y los vectores. No hemos considerado esas construcciones como una creacin de un nuevo tipo de dato sino como una coleccin ordenada y homognea de una cantidad fija de elementos, todos ellos del mismo tipo, y referenciados uno a uno mediante ndices. Pero existe otro modo, en C, de crear un tipo de dato estructurado. Y a ese nos queremos referir cuando decimos que creamos un nuevo tipo de dato, y no solamente una coleccin ordenada de elementos del mismo tipo. Ese tipo de dato se llama registro, y est formado por yuxtaposicin de elementos que contienen informacin relativa a una

579

Informtica Aplicada. Programacin en Lenguaje C.

misma entidad. Por ejemplo, el tipo de dato asignatura puede tener diferentes elementos, todos ellos relativos a la entidad asignatura, y no todos ellos del mismo tipo. Y as, ese tipo de dato registro que hemos llamado asignatura tendra un elemento que llamaramos clave y que podra ser de tipo long; y otro campo se llamara descripcin y sera de tipo char*; y un tercer elemento sera el nmero de crditos y sera de tipo float, etc. A cada elemento de un registro se le llama campo. Un registro es un tipo de dato estructurado heterogneo, donde no todos los elementos (campos) son del mismo tipo. El dominio de este tipo de dato est formado por el producto cartesiano de los diferentes dominios de cada uno de los componentes. Y el modo de referenciar a cada campo dentro del registro es mediante el nombre que se le d a cada campo. En C, se dispone de una palabra reservada para la creacin de registros: la palabra struct.

Estructuras de datos en C.
Una estructura de datos en C es una coleccin de variables, no necesariamente del mismo tipo, que se referencian con un nombre comn. Lo normal ser crear estructuras formadas por variables que tengan alguna relacin entre s, de forma que se logra compactar la informacin, agrupndola de forma cabal. Cada variable de la estructura se llama, en el lenguaje C, elementos de la estructura. Este concepto es equivalente al presentado antes al hablar de campos. La sintaxis para la creacin de estructuras presenta diversas formas. Empecemos viendo una de ellas: struct nombre_estructura { tipo_1 identificador_1; tipo_2 identificador_2; ... tipo_N identificador_N;

580

Captulo 20. Estructuras estticas de datos y definicin de tipos.

}; La definicin de la estructura termina, como toda sentencia de C, en un punto y coma. Una vez se ha creado la estructura, y al igual que hacamos con las uniones, podemos declarar variables del nuevo tipo de dato dentro de cualquier funcin del programa donde est definida la estructura: struct nombre_estructura variable_estructura; Y el modo en que accedemos a cada uno de los elementos (o campos) de la estructura (o registro) ser mediante el operador miembro, que se escribe con el identificador punto (.): variable_estructura.identificador_1 Y, por ejemplo, para introducir datos en la estructura haremos: variable_estructura.identificador_1 = valor_1; La declaracin de una estructura se hace habitualmente fuera de cualquier funcin, puesto que el tipo de dato trasciende el mbito de una funcin concreta. De todas formas, tambin se puede crear el tipo de dato dentro de la funcin, cuando ese tipo de dato no va a ser empleado ms all de esa funcin. En ese caso, quiz no sea necesario siquiera dar un nombre a la estructura, y se pueden crear directamente las variables que deseemos de ese tipo, con la siguiente sintaxis: struct { tipo_1 identificador_1; tipo_2 identificador_2; ... tipo_N identificador_N; }nombre_variable; Y as queda definida la variable nombre_variable, de tipo de dato struct. Evidentemente, esta declaracin puede hacerse fuera de cualquier funcin, de forma que la variable que generemos sea de mbito global.

581

Informtica Aplicada. Programacin en Lenguaje C.

Otro modo de generar la estructura y a la vez declarar las primeras variables de ese tipo, ser la siguiente sintaxis: struct nombre_estructura { tipo_1 identificador_1; tipo_2 identificador_2; ... tipo_N identificador_N; }variable_1, ..., variable_N; Y as queda definido el identificador nombre_estructura y quedan declaradas las variables variable_1, , variable_N, que sern locales o globales segn se hay hecho esta declaracin en uno u otro mbito. Lo que est claro es que si la declaracin de la estructura se realiza dentro de una funcin, entonces nicamente dentro de su mbito el identificador de la estructura tendr el significado de tipo de dato, y solamente dentro de esa funcin se podrn utilizar variables de ese tipo estructurado. El mtodo ms cmodo para la creacin de estructuras en C es mediante la combinacin de la palabra struct de la palabra typedef. La sintaxis de esa forma de creacin es la siguiente: typedef struct { tipo_1 identificador_1; tipo_2 identificador_2; ... tipo_N identificador_N; } nombre_estructura; Y as, a partir de este momento, en cualquier lugar del mbito de esta definicin del nuevo tipo de dato, podremos crear variables con la siguiente sintaxis: nombre_estructura nombre_variable; Veamos algn ejemplo: podemos necesitar definir un tipo de dato que podamos luego emplear para realizar operaciones en variable compleja.

582

Captulo 20. Estructuras estticas de datos y definicin de tipos.

Esta estructura, que podramos llamar complejo, tendra la siguiente forma: typedef struct { double real; double imag; }complejo; Y tambin podramos definir una serie de operaciones, mediante funciones: por ejemplo, la suma, la resta y el producto de complejos. El programa completo podra tener la siguiente forma: #include <stdio.h> typedef struct { double real; double imag; }complejo; complejo sumac(complejo, complejo); complejo restc(complejo, complejo); complejo prodc(complejo, complejo); void mostrar(complejo); int main (void) { complejo A, B, C; printf("Introduccin de datos ... printf("Parte real de A ......... scanf( %lf",&A.real); printf("Parte imaginaria de A ... scanf( %lf",&A.imag); printf("Parte real de B ......... scanf( %lf",&B.real); printf("Parte imaginaria de B ... scanf( %lf",&B.imag); // SUMA ... printf("\n\n"); mostrar(A); printf(" + "); mostrar(B); C = sumac(A,B); mostrar(C); // RESTA ... printf("\n\n"); mostrar(A); printf(" - "); mostrar(B);

\n"); "); "); "); ");

583

Informtica Aplicada. Programacin en Lenguaje C.

C = restc(A,B); mostrar(C); // PRODUCTO ... printf("\n\n"); mostrar(A); printf(" * "); mostrar(B); C = prodc(A,B); mostrar(C); return 0; } complejo sumac(complejo c1, complejo c2) { c1.real += c2.real; c1.imag += c2.imag; return c1; } complejo restc(complejo c1, complejo c2) { c1.real -= c2.real; c1.imag -= c2.imag; return c1; } complejo prodc(complejo c1, complejo c2) { complejo S; S.real = c1.real + c2.real; - c1.imag * c2.imag; S.imag = c1.real + c2.imag + c1.imag * c2.real; return S; } void mostrar(complejo X) { printf("(% .2lf%s%.2lf * i) ", X.real, X.imag > 0 ? " +" : " " , X.imag); } As podemos ir definiendo un nuevo tipo de dato, con un dominio que es el producto cartesiano del dominio de los double consigo mismo, y con unos operadores definidos mediante funciones. Las nicas operaciones que se pueden hacer sobre la estructura (aparte de las que podamos definir mediante funciones) son las siguientes:

584

Captulo 20. Estructuras estticas de datos y definicin de tipos.

operador direccin (&), porque toda variable, tambin las estructuradas, tienen una direccin en la memoria; operador seleccin (.) mediante el cual podemos acceder a cada uno de los elementos de la estructura; y operador asignacin, que slo puede de forma que los dos extremos de la asignacin (tanto el Lvalue como el Rvalue) sean variables objeto del mismo tipo. Por ejemplo, se puede hacer la asignacin: complejo A, B; A.real = 2; A.imag = 3; B = A; Y as, las dos variables valen lo mismo: al elemento real de B se le asigna el valor consignado en la parte real de A; y al elemento imag de B se le asigna el valor del elemento imag de A. Otro ejemplo de estructura podra ser el que antes hemos iniciado, al hablar de los registros: una estructura para definir un tipo de dato que sirva para el manejo de asignaturas: typedef struct { long clave; char descripcion[50]; float creditos; }asignatura;

Vectores y punteros a estructuras.


Una vez hemos creado un nuevo tipo de dato estructurado, no resulta extrao que podamos crear vectores y matrices de este nuevo tipo de dato. Si, por ejemplo, deseamos hacer un inventario de asignaturas, ser lgico que creemos un array de tantas variables asignatura como sea necesario. asignatura curricula[100];

585

Informtica Aplicada. Programacin en Lenguaje C.

Y as, tenemos 100 variables del tipo asignatura, distribuidas en la memoria de forma secuencial, una despus de la otra. El modo en que accederemos a cada una de las variables ser, como siempre mediante la operatoria de ndices: curricula[i]. Y si queremos acceder a algn miembro de una variable del tipo estructurado, utilizaremos de nuevo el operador de miembro: curricula[i].descripcin. Tambin podemos trabajar con operatoria de punteros. As como antes hemos hablado de curricula[i], tambin podemos llegar a esa variable del array con la expresin *(curricula + i). De nuevo, todo es igual. Donde hay un cambio es en el operador de miembro: si trabajamos con operatoria de punteros, el operador de miembro ya no es el punto, sino que est formado por los caracteres ->. Si queremos hacer referencia al elemento o campo descripcion de una variable del tipo asignatura, la sintaxis ser: *(curricula + i)->descripcion. Y tambin podemos trabajar con asignacin dinmica de memoria. En ese caso, se declara un puntero del tipo estructurado, y luego se le asigna la memoria reservada mediante la funcin malloc. Si creamos un array de asignaturas en memoria dinmica, un programa de gestin de esas asignaturas podra ser el siguiente: #include <stdio.h> #include <stdlib.h> typedef struct { long clave; char descr[50]; float cred; }asig; int main(void) { asig *curr; short n, i; printf("Indique n de asignaturas de su CV ... "); scanf( %hd",&n); /* La variable n recoge el nmero de elementos de tipo asignatura que debe tener nuestro array. */

586

Captulo 20. Estructuras estticas de datos y definicin de tipos.

curr = (asig*)malloc(n * sizeof(asig)); if(curr == NULL) { printf("Memoria insuficiente.\n"); printf("La ejecucion se interrumpira.\n"); printf("Pulse una tecla para terminar ... "); getchar(); exit(0); } for(i = 0 ; i < n ; i++) { printf("\n\nAsignatura %hd ... \n",i printf("clave ......... "); scanf( %ld",&(curr + i)->clave); printf("Descripcion ... "); gets((curr + i)->descr); printf("creditos ...... "); scanf( %f",&(curr + i)->cred); }

+ 1);

// Listado ... for(i = 0 ; i < n ; i++) { printf("(%10ld)\t",(curr + i)->clave); printf("%s\t",(curr + i)->descr); printf("%4.1f creditos\n",(curr + i)->cred); } return 0; } Observamos que (curr + i) es la direccin de la posicin i-sima del vector curr. Es, pues, una direccin. Y (curr + i)->clave es el valor del campo clave de la variable que est en la posicin i-sima del vector curr. Es, pues, un valor: no es una direccin. Y (curr + i)->descr es la direccin de la cadena de caracteres que forma el campo descr de la variable que est en la posicin i-sima del vector curr. Es, pues, una direccin, porque direccin es el campo descr: un array de caracteres. Que accedamos a la variable estructura a travs de un puntero o a travs de su identificador influye nicamente en el operador de miembro que vayamos a utilizar. Una vez tenemos referenciado a travs de la estructura un campo o miembro concreto, ste ser tratado como

587

Informtica Aplicada. Programacin en Lenguaje C.

direccin o como valor dependiendo de que el miembro se haya declarado como puntero o como variable de dato.

Anidamiento de estructuras.
Podemos definir una estructura que tenga entre sus miembros una variable que sea tambin de tipo estructura. Por ejemplo: typedef struct { unsigned short dia; unsigned short mes; unsigned short anyo; }fecha; typedef struct { unsigned long clave; char descripcion[50]; double creditos; fecha convocatorias[3]; }asignatura; Ahora a la estructura de datos asignatura le hemos aadido un vector de tres elementos para que pueda consignar sus fechas de exmenes en las tres convocatorias. EL ANSI C permite hasta 15 niveles de anidamiento de estructuras. El modo de llegar a cada campo de la estructura fecha es, como siempre, mediante los operadores de miembro. Por ejemplo, si queremos que la primera convocatoria se realice el 15 de enero del presente ao, la segunda convocatoria el 21 de junio y la tercera el 1 de septiembre, las rdenes debern ser: #include <time.h> #include <stdio.h> #include <string.h> typedef struct { unsigned short dia; unsigned short mes; unsigned short anyo; }fecha;

588

Captulo 20. Estructuras estticas de datos y definicin de tipos.

typedef struct { unsigned long clave; char descripcion[50]; double creditos; fecha c[3]; }asignatura; int main(void) { asignatura asig; time_t bloquehoy; struct tm *hoy; bloquehoy = time(NULL); hoy = localtime(&bloquehoy); asig.clave = 10102301; *asig.descripcion = '\0'; strcat(asig.descripcion,"fundamentos de informtica"); asig.creditos = 7.5; asig.c[0].dia = 15; asig.c[0].mes = 1; asig.c[0].anyo = hoy->tm_year - 100; asig.c[1].dia = 21; asig.c[1].mes = 6; asig.c[1].anyo = hoy->tm_year - 100; asig.c[2].dia = 1; asig.c[2].mes = 9; asig.c[2].anyo = hoy->tm_year - 100; printf("Asignatura %10ld\n",asig.clave); printf("%s\t",asig.descripcion); printf("%4.1lf\n", asig.creditos); printf("\npriemra convocatoria ... "); printf("%2hu-%2hu-%02hu", asig.c[0].dia, asig.c[0].mes,asig.c[0].anyo); printf("\nsegunda convocatoria ... "); printf("%2hu-%2hu-%02hu", asig.c[1].dia, asig.c[1].mes,asig.c[1].anyo); printf("\ntercera convocatoria ... "); printf("%2hu-%2hu-%02hu", asig.c[2].dia, asig.c[2].mes,asig.c[2].anyo); return 0; }

589

Informtica Aplicada. Programacin en Lenguaje C.

Hemos asignado a cada uno de los tres elementos del vector c los valores de da, mes y ao correspondientes a cada una de las tres convocatorias. Hemos utilizado ndices de vectores para referenciar cada una de las tres fechas. Podramos haber trabajado tambin con operatoria de punteros. Por ejemplo, la referencia al da de la segunda convocatoria es, con operatoria de ndices asig.c[1].dia = 21; y mediante operatoria de punteros: (asig.c + 1)->dia = 21; Donde asig.c es la direccin del primer elemento del vector c. Y donde (asig.c + 1) es la direccin del segundo elemento del vector c. Y donde (asig.c + 1)->dia es el valor del campo da de la variable de tipo fecha cuya direccin es (asig.c + 1). Respecto a la funcin localtime, el tipo de dato estructurado tm, y sus campos (entre ellos el campo tm_year) puede encontrarse abundante informacin sobre todo ello en la ayuda on line que ofrece cualquier compilador. Son definiciones que vienen recogidas en la biblioteca time.h y son estndares de ANSI C.

Tipo de dato unin.


Adems de las estructuras, el lenguaje C permite otra forma de creacin de un nuevo tipo de dato: mediante la creacin de una unin (que se define mediante la palabra clave en C union: por cierto, con sta, acabamos de hacer referencia en este manual a la ltima de las 32 palabras del lxico del lenguaje C). Una unin es una posicin de memoria compartida por dos o ms variables diferentes, y en general de distinto tipo. Es una regin de memoria que, a lo largo del tiempo, puede contener objetos de diversos

590

Captulo 20. Estructuras estticas de datos y definicin de tipos.

tipos. Una unin permite almacenar tipos de dato diferentes en el mismo espacio de memoria. Como las estructuras, las uniones tambin tienen miembros; pero a diferencia de las estructuras, donde la memoria que ocupan es igual a la suma del tamao de cada uno de sus campos, la memoria que emplea una variable de tipo unin es la necesaria para el miembro de mayor tamao dentro de la unin. La unin almacena nicamente uno de los valores definidos en sus miembros. La sintaxis para la creacin de una unin es muy semejante a la empleada para la creacin de una estructura: typedef union { tipo_1 identificador_1; tipo_2 identificador_2; ... tipo_N identificador_N; } nombre_union; O en cualquiera otra de las formas que hemos visto para la creacin de estructuras. Es responsabilidad del programador mantener la coherencia en el uso de esta variable: si la ltima vez que se asign un valor a la unin fue sobre un miembro de un determinado tipo, luego, al acceder a la informacin de la unin, debe hacerse con referencia a un miembro de un tipo de dato adecuado y coherente con el ltimo que se emple. No tendra sentido almacenar un dato de tipo float de uno de los campos de la unin y luego querer leerlo a travs de un campo de tipo char. El resultado de una operacin de este estilo es imprevisible. Veamos un ejemplo, y comparmoslo con una estructura de definicin similar: #include <stdio.h> #include <string.h> typedef union { long dni; char ss[30]; }ident1;

// nmero de dni. // nmero de la seg. social.

591

Informtica Aplicada. Programacin en Lenguaje C.

typedef struct { long dni; char ss[30]; }ident2; int main(void) { ident1 id1; ident2 id2;

// nmero de dni. // nmero de la seg. social.

printf("tamao de la union: %ld\n",sizeof(ident1)); printf("tamao de la estructura:%ld\n",sizeof(ident2)); // Datos de la estructura ... id2.dni = 44561098; *(id2.ss + 0) = NULL; strcat(id2.ss,"12/0324/5431890"); printf("\nid2.dni = %ld\n",id2.dni); printf("id2.ss = %s\n",id2.ss); // Datos de la unin ... *(id1.ss + 0) = NULL; strcat(id1.ss,"12/0324/5431890"); printf("\nid1.dni = %ld (mal)\n",id1.dni);// Mal. printf("id1.ss = %s\n",id1.ss); id1.dni = 44561098; printf("\nid1.dni = %ld\n",id1.dni); printf("id1.ss = %s(mal)\n",id1.ss); // Mal. return 0; } El programa ofrece la siguiente salida por pantalla; tamao de la union: 30 tamao de la estructura:34 id2.dni = 44561098 id2.ss = 12/0324/5431890 id1.dni = 808399409 (mal) id1.ss = 12/0324/5431890 id1.dni = 44561098 id1.ss = 324/5431890 (mal) El tamao de la estructura es la suma del tamao de sus miembros. El tamao de la unin es el tamao del mayor de sus miembros.

592

Captulo 20. Estructuras estticas de datos y definicin de tipos.

En la estructura se tienen espacios disjuntos para cada miembro: por un lado se almacena el valor long de la variable dni y por otro la cadena de caracteres ss. En la unin, si la ltima asignacin se ha realizado sobre la cadena, no tiene sentido que se pretenda obtener el valor del miembro long dni; Y si la ltima asignacin se ha realizado sobre el campo dni, tampoco tiene sentido pretender leer el valor de la cadena ss. Una buena prueba de que la unin comparte la memoria la tenemos en el ejemplo donde ha quedado como mal uso la impresin del dni. Si vemos el valor impreso (808399409) y lo pasamos a hexadecimal tenemos 302F3231. Y si separamos esa cifra en pares, tendremos cuatro pares: el 30 (que es el cdigo ASCII del carcter 0); el 2F (que es el cdigo ASCII del carcter /); el 32 (que es el cdigo ASCII del carcter 2); y el 31, que es el cdigo ASCII del carcter 1). Y si vemos el valor de los cuatro elementos de la cadena, tenemos que son 12/0. Precisamente los cuatro que antes hemos reconocido en la codificacin del mal ledo entero. Y es que hemos ledo los cuatro primeros caracteres de la cadena como si de un entero se tratase. En este aspecto, una buena utilidad del mal uso de las uniones (en ese caso no sera mal uso: sera una treta del programador) ser el poder obtener el cdigo interno de la informacin de los valores de tipo float. Veamos cmo podramos hacerlo: #include <stdio.h> typedef union { float fvalor; unsigned long lvalor; }codigo; int main(void) { unsigned long Test = 0x80000000; codigo a; printf("Introduzca float del que desea "); printf("conocer su codificacion binaria ... "); scanf( %f", &a.fvalor);

593

Informtica Aplicada. Programacin en Lenguaje C.

while(Test) { printf("%c",a.lvalor & Test ? '1' : '0'); Test >>= 1; } return 0; } La funcin principal lee el valor y lo almacena en el campo de tipo float. Pero luego lo lee como si fuese un dato de tipo long. Y si a ese valor long le aplicamos el operador and a nivel de bit (permitido en las variables long, y no permitido en las float) llegamos a poder obtener la codificacin interna de los valores en coma flotante. No hemos explicado en este manual la norma IEEE 754 que emplean los PC para codificar esa clase de valores, pero quien quiera conocerla y cotejarla bien puede hacerlo: la norma est fcilmente accesible; y el programa que acabamos de presentar permite la visualizacin de esa codificacin.

Ejercicios.

20.1.

Definir un tipo de dato entero, de longitud tan grande como se quiera, y definir tambin los operadores bsicos de ese nuevo tipo de dato mediante la definicin y declaracin de las funciones que sean necesarias.

La estructura que define el nuevo tipo de dato podra ser como la que sigue: #define Byte4 32 typedef unsigned long int UINT4; typedef struct { /* nmero de elementos UINT4 reservados. */ UINT4 D;

594

Captulo 20. Estructuras estticas de datos y definicin de tipos.

/* nmero de elementos UINT4 utilizados actualmente. */ UINT4 T; /* nmero de bits significativos. */ UINT4 B; /* El nmero, que tendr tantos elementos como indique D. */ UINT4 *N; }NUMERO; Que tiene cuatro elementos, que servirn para conocer bien las propiedades del nmero (nuevo tipo de dato entero) y que nos permitirn agilizar luego numerosas operaciones con ellos. El puntero N recoger un array (en asignacin dinmica) donde se codificarn los nmeros; a este puntero se le asignan tantos elementos enteros largos consecutivos como indique el campo D. Y una vez creado el nmero (reservada la memoria), siempre convendr mantener actualizado el valor de T y de B: el nmero de elementos enteros largos que realmente se emplean en cada momento para la codificacin del entero largo; y el nmero de bits empleados en la codificacin del nmero actual. Podemos ahora definir una serie de operadores, empleando funciones. Lo primero ser definir la funcin que asigne memoria al puntero N y aquella que ponga a cero el nuevo entero creado. A una la llamamos PonerACero, y a la otra CrearNumero: void PonerACero(NUMERO*n) { /* Esta funcin pone a cero los campos B y T de la variable NUMERO recibida por referencia. Y deja tambin a cero todos los dgitos del nmero. Considera que el campo T viene correctamente actualizado. */ n->B = 0; while(n->T) *(n->N + --n->T) = 0x0; }

#define msg01_001 \ "01_001: Error de asignacin de memoria en CrearNumero()\n" void CrearNumero(NUMERO*n) { /* Con esta funcin asignamos una cantidad de memoria a n->N (tantos elementos UINT4 como indique n->D) */ n->N = (UINT4*)malloc(n->D * sizeof(UINT4)); if(n->N == NULL)

595

Informtica Aplicada. Programacin en Lenguaje C.

{ printf(msg01_001); exit(1); } n->T = n->D; PonerACero(n); } Y podemos definir ahora otras funciones, necesarias para trabajar cmodamente en este nuevo tipo de dato. Vamos introducindolas una a una: Funcin diseada para copiar el valor de un NUMERO en otro NUMERO. No bastara hacer copia mediante el asignacin direccin, porque en ese caso se copiara la direccin de N de uno a otro pero lo que nos interesa es que sean variables diferentes con direcciones diferentes, que codifiquen el mismo nmero; y se copiaran los valores del campo D, y eso no nos interesa porque el campo D indica cuntos elemento de tipo UINT4 se han reservado en el puntero, que depende de cada NUMERO. La funcin queda: #define msg01_002 \ "01_002: No se puede hacer copia de la variable. Error en CopiarNumero()\n" void CopiarNumero(NUMERO*original,NUMERO*copia) { /* Esta funcin copia el valor de nmero grande original->N en copia->N. Previo a esta operacin, verifica que el tamao del original no supera la capacidad (copia->D) de la copia.*/ UINT4 c; if(original->T > copia->D) { printf(msg01_002); exit(1); } /* Si el original y la copia no son la misma variable ... */ if(original->N != copia->N) { PonerACero(copia); for(c = 0 ; c < original->T ; c++) *(copia->N + c) = *(original->N + c); copia->T = original->T; copia->B = original->B; } }

596

Captulo 20. Estructuras estticas de datos y definicin de tipos.

Funcin que actualice los valores de los campos B y T: void longitud(NUMERO*n) { /* De entrada se le supone el tamao de la dimensin ----- */ n->T = n->D; while(*(n->N + n->T - 1) == 0 && n->T != 0) (n->T)--; /* Una vez tenemos determinado el nmero de elementos UINT4 que forman el nmero, vamos ahora a calcular el nmero de bits a partir del ms significativo. */ n->B = Byte4 * n->T; if(n->B) { UINT4 Test = 0x80000000; while(!(*(n->N + n->T - 1) & Test)) { Test >>= 1; n->B--; } } } La siguiente funcin lo que hace es intercambiar los valores de dos variables NUMERO: #define msg01_003 "01_003: No se pueden intercambiar valores. Error en inv_v()\n" void inv_v(NUMERO*n1,NUMERO*n2) { /* Si los dos tamaos de los arrays son iguales, entonces intercambiamos los punteros de los nmeros. */ if(n1->D == n2->D) { UINT4*aux; aux = n1->N; n1->N = n2->N; n2->N = aux; } /* En caso contrario ... */ else { UINT4 L; L = (n1->T >= n2->T) ? n1->T : n2->T; if(n1->T > n2->D || n2->T > n1->D) { printf(msg01_003); exit(1); } /* Intercambiamos cada uno de los dgitos */ \

597

Informtica Aplicada. Programacin en Lenguaje C.

while(L) { L--; *(n1->N + L) ^= *(n2->N + L); *(n2->N + L) ^= *(n1->N + L); *(n1->N + L) ^= *(n2->N + L); } } /* Intercambiamos los valores de los tamaos. */ n1->T ^= n2->T; n2->T ^= n1->T; n1->T ^= n2->T; /* Intercambiamos los valores del nmero de bits. */ n1->B ^= n2->B; n2->B ^= n1->B; n1->B ^= n2->B; /* Evidentemente, no podemos intercambiar las dimensiones en que han sido definidos cada uno de los dos nmeros grandes.*/ } Y la siguiente funcin determina cul de los dos NUMEROs que se reciben como parmetro es mayor y cul menor: typedef signed short int SINY2; typedef signed long int SINT4; SINT2 orden(NUMERO*n1,NUMERO*n2) { /* Devuelve +1 si n1 > n2. -1 si n1 < n2. 0 si n1 == n2. */ SINT4 c; if(n1->B < n2->B) return -1; /* Es decir, n1 < n2. if(n1->B > n2->B) return +1; /* Es decir, n1 > n2. /* LLegados aqu tenemos que *n1->T es igual for(c = n1->T - 1 ; c >= 0 ; c--) { if(*(n1->N + c) < *(n2->N + c)) return -1; /* Es decir, n1 if(*(n1->N + c) > *(n2->N + c)) return +1; /* Es decir, n1 } return 0; }

*/ */ a n2->T... */

< n2. */ > n2. */

Podemos seguir definiendo funciones auxiliares, como desplazamiento a izquierda o desplazamiento a derecha, etc. Lo dejamos como ejercicio para resolver. Tambin queda pendiente trabajar las operaciones

598

Captulo 20. Estructuras estticas de datos y definicin de tipos.

aritmticas de suma, resta, producto, cociente, mdulo, etc. No es objeto del libro mostrar todo ese cdigo. Hemos mostrado el que precede porque est formado en su mayor parte por funciones sencillas y fciles de implementar y porque todas ellas trabajan con estructuras.

599

Informtica Aplicada. Programacin en Lenguaje C.

600

CAPTULO 21
GESTIN DE ARCHIVOS.
Hasta el momento, toda la informacin (datos) que hemos sido capaces de gestionar, la hemos tomado de dos nicas fuentes: o eran datos del programa, o eran datos que introduca el usuario desde el teclado. Y hasta el momento, siempre que un programa ha obtenido un resultado, lo nico que hemos hecho ha sido mostrarlo en pantalla. Y, desde luego, sera muy interesante poder almacenar la informacin generada por un programa, de forma que esa informacin pudiera luego ser consultada por otro programa, o por el mismo u otro usuario. O sera muy til que la informacin que un usuario va introduciendo por consola quedase almacenada para sucesivas ejecuciones del programa o para posibles manipulaciones de esa informacin. En definitiva, sera muy conveniente poder almacenar en algn soporte informtico (por ejemplo, en el disco del ordenador) esa informacin, y poder luego acceder a ese disco para volver a tomarla, para actualizar la

Informtica Aplicada. Programacin en Lenguaje C.

informacin almacenada, para aadir o para eliminar todo o parte de ella. Y eso es lo que vamos a ver en este tema: la gestin de archivos. Comenzaremos con una breve presentacin de carcter terico sobre los archivos y pasaremos a ver despus el modo en que podemos emplear los distintos formatos de archivo.

Tipos de dato con persistencia.


Entendemos por tipo de dato con persistencia, o archivo, o fichero aquel cuyo tiempo de vida no est ligado al de ejecucin del programa que lo crea o lo maneja. Es decir, se trata de una estructura de datos externa al programa, que lo trasciende. Un archivo existe desde que un programa lo crea y mientras que no sea destruido por este u otro programa. Un archivo est compuesto por registros homogneos que llamamos registros de archivo. La informacin de cada registro viene recogida mediante campos. Es posible crear ese tipo de dato con persistencia porque esa informacin queda almacenada sobre una memoria externa. Los archivos se crean sobre dispositivos de memoria masiva. El lmite de tamao de un archivo viene condicionado nicamente por el lmite de los dispositivos fsicos que lo albergan. Los programas trabajan con datos que residen en la memoria principal del ordenador. Para que un programa manipule los datos almacenados en un archivo y, por tanto, en un dispositivo de memoria masiva, esos datos deben ser enviados desde esa memoria externa a la memoria principal mediante un proceso de extraccin. Y de forma similar, cuando los datos que manipula un programa deben ser concatenados con los del archivo se utiliza el proceso de grabacin.

602

Captulo 21. Gestin de archivos.

De hecho, los archivos se conciben como estructuras que gozan de las siguientes caractersticas: 1. Capaces de contener grandes cantidades de informacin. 2. Capaces de y sobrevivir a los procesos que lo generan y utilizan. 3. Capaces de ser accedidos desde diferentes procesos o programas. Desde el punto de vista fsico, o del hardware, un archivo tiene una direccin fsica: en el disco toda la informacin se guarda (grabacin) o se lee (extraccin) en bloques unidades de asignacin o clusters referenciados por un nombre de unidad o disco, la superficie a la que se accede, la pista y el sector: todos estos elementos caracterizan la direccin fsica del archivo y de sus elementos. Habitualmente, sin embargo, el sistema operativo simplifica mucho esos accesos al archivo, y el programador puede trabajar con un concepto simplificado de archivo o fichero: cadena de bytes consecutivos terminada por un carcter especial llamado EOF (End Of File); ese carcter especial (EOF) indica que no existen ms bytes de informacin ms all de l. Este segundo concepto de archivo permite al usuario trabajar con datos persistentes sin tener que estar pendiente de los problemas fsicos de almacenamiento. El sistema operativo posibilita al programador trabajar con archivos de una forma sencilla. El sistema operativo hace de interfaz entre el disco y el usuario y sus programas. 1. Cada vez que accede a un dispositivo de memoria masiva para leer o paras grabar, el sistema operativo transporta, desde o hasta la memoria principal, una cantidad fija de informacin, que se llama bloque o registro fsico y que depende de las caractersticas fsicas del citado dispositivo. En un bloque o registro fsico puede haber varios registros de archivo, o puede que un registro de archivo ocupe varios bloques. Cuantos ms registros de archivo quepan en cada bloque menor ser el nmero de accesos necesarios al dispositivo de

603

Informtica Aplicada. Programacin en Lenguaje C.

almacenamiento fsico para lograr procesar toda la informacin del archivo. 2. El sistema operativo tambin realiza la necesaria transformacin de las direcciones: porque una es la posicin real o efectiva donde se encuentra el registro dentro del soporte de informacin (direccin fsica o direccin hardware) y otra distinta es la posicin relativa que ocupa el registro en nuestro archivo, tal y como es visto este archivo por el programa que lo manipula (direccin lgica o simplemente direccin). 3. Un archivo es una estructura de datos externa al programa. Nuestros programas acceden a los archivos para leer, modificar, aadir, o eliminar registros. El proceso de lectura o de escritura tambin lo gobierna el sistema operativo. Al leer un archivo desde un programa, se transfiere la informacin, de bloque en bloque, desde el archivo hacia una zona reservada de la memoria principal llamada buffer, y que est asociada a las operaciones de entrada y salida de archivo. Tambin se acta a travs del buffer en las operaciones de escritura sobre el archivo.

Archivos y sus operaciones.


Antes de abordar cmo se pueden manejar los archivos en C, ser conveniente hacer una breve presentacin sobre los archivos con los que vamos a trabajar: distintos modos en que se pueden organizar, y qu operaciones se pueden hacer con ellos en funcin de su modo de organizacin. Hay diferentes modos de estructurar o de organizar un archivo. Las caractersticas del archivo y las operaciones que con l se vayan a poder realizar dependen en gran medida de qu modo de organizacin se adopte. Las dos principales formas de organizacin que vamos a ver en este manual son:

604

Captulo 21. Gestin de archivos.

1. Secuencial. Los registros se encuentran en un orden secuencial, de forma consecutiva. Los registros deben ser ledos, necesariamente, segn ese orden secuencial. Es posible leer o escribir un cierto nmero de datos comenzando siempre desde el principio del archivo. Tambin es posible aadir datos a partir del final del archivo. El acceso secuencial es una forma de acceso sistemtico a los datos poco eficiente si se quiere encontrar un elemento particular. 2. Indexado. Se dispone de un ndice para obtener la ubicacin de cada registro. Eso permite localizar cualquier registro del archivo sin tener que leer todos los que le preceden. La decisin sobre cul de las dos formas de organizacin tomar depender del uso que se d al archivo. Para poder trabajar con archivos secuenciales, se debe previamente asignar un nombre o identificador a una direccin de la memoria externa (a la que hemos llamado antes direccin hardware). Al crear ese identificador se define un indicador de posicin de archivo que se coloca en esa direccin inicial. Al iniciar el trabajo con un archivo, el indicador se coloca en el primer elemento del archivo que coincide con la direccin hardware del archivo. Para extraer un registro del archivo, el indicador debe previamente estar ubicado sobre l; y despus de que ese elemento es ledo o extrado, el indicador se desplaza automticamente al siguiente registro de la secuencia. Para aadir nuevos registros primero es necesario que el indicador se posicione o apunte al final del archivo. Conforme el archivo va creciendo de tamao, a cada nuevo registro se le debe asignar nuevo espacio en esa memoria externa. Y si el archivo est realizando acceso de lectura, entonces no permite el de escritura; y al revs: no se pueden utilizar los dos modos de acceso (lectura y escritura) de forma simultnea.

605

Informtica Aplicada. Programacin en Lenguaje C.

Las operaciones que se pueden aplicar sobre un archivo secuencial son: 1. Creacin de un nuevo archivo vaco. El archivo es una secuencia vaca: (). 2. Adicin de registros mediante buffer. La adicin almacenar un registro nuevo concatenado con la secuencia actual. El archivo pasa a ser la secuencia (secuencia inicial + buffer). La informacin en un archivo secuencial solo es posible aadirla al final del archivo. 3. Inicializacin para comenzar luego el proceso de extraccin. Con esta operacin se coloca el indicador sobre el primer elemento de la secuencia, dispuesto as para comenzar la lectura de registros. El archivo tiene entonces la siguiente estructura: Izquierda = (); Derecha = (secuencia); Buffer = primer elemento de (secuencia). 4. Extraccin o lectura de registros. Esta operacin coloca el indicador sobre el primer elemento o registro de la parte derecha del archivo y concatena luego el primer elemento de la parte derecha al final de la parte izquierda. Y eso de forma secuencial: para leer el registro n en el archivo es preciso leer previamente todos los registros desde el 1 hasta el n 1. Durante el proceso de extraccin hay que verificar, antes de cada nueva lectura, que no se ha llegado todava al final del archivo y que, por tanto, la parte derecha an no es la secuencia vaca. Y no hay ms operaciones. Es decir, no se puede definir ni la operacin insercin de registro, ni la operacin modificacin de registro, ni la operacin borrado de registro. Al menos diremos que no se realizan fcilmente. La operacin de insercin se puede realizar creando de hecho un nuevo archivo. La modificacin se podr hacer si al realizar la modificacin no se aumenta la longitud del registro. Y el borrado no es posible y, por tanto, en los archivos secuenciales se define el borrado lgico: marcar el registro de tal forma que esa marca se interprete como elemento borrado.

606

Captulo 21. Gestin de archivos.

Archivos de texto y binarios.


Decamos antes que un archivo es un conjunto de bytes secuenciales, terminados por el carcter especial EOF. Si nuestro archivo es de texto, esos bytes sern interpretados como caracteres. Toda la informacin que se puede guardar en un archivo de texto son caracteres. Esa informacin podr por tanto ser visualizada por un editor de texto. Si se desean almacenar los datos de una forma ms eficiente, se puede trabajar con archivos binarios. Los nmeros, por ejemplo, no se almacenan como cadenas de caracteres, sino segn la codificacin interna que use el ordenador. Esos archivos binarios no pueden visualizarse mediante un editor de texto. Si lo que se desea es que nuestro archivo almacene una informacin generada por nuestro programa y que luego esa informacin pueda ser, por ejemplo, editada, entonces se deber trabajar con ficheros de caracteres o de texto. Si lo que se desea es almacenar una informacin que pueda luego ser procesada por el mismo u otro programa, entonces es mejor trabajar con ficheros binarios.

Tratamiento de archivos en el lenguaje C.


Ya hemos visto todas las palabras reservadas de C. Ninguna de ellas hace referencia a operacin alguna de entrada o salida de datos. Todas las operaciones de entrada y salida estn definidas mediante funciones de biblioteca estndar. Para trabajar con archivos con buffer, las funciones, tipos de dato predefinidos y constantes estn recogidos en la biblioteca stdio.h. Para trabajar en entrada y salida de archivos sin buffer estn las funciones definidas en io.h.

607

Informtica Aplicada. Programacin en Lenguaje C.

Todas las funciones de stdio.h de acceso a archivo trabajan mediante una interfaz que est localizada por un puntero. Al crear un archivo, o al trabajar con l, deben seguirse las normas que dicta el sistema operativo. De trabajar as se encargan las funciones ya definidas, y esa gestin es transparente para el programador. Esa interfaz permite del que el trabajo final de acceso donde al se archivo realizan sea las

independiente

dispositivo

fsico

operaciones de entrada o salida. Una vez el archivo ha quedado abierto, se puede intercambiar informacin entre ese archivo y el programa. El modo en que la interfaz gestiona y realiza ese trfico es algo que no afecta para nada a la programacin. Al abrir, mediante una funcin, un archivo que se desee usar, se indica, mediante un nombre, a qu archivo se quiere acceder; y esa funcin de apertura devuelve al programa una direccin que deber emplearse en las operaciones que se realicen con ese archivo desde el programa. Esa direccin se recoge en un puntero, llamado puntero de archivo. Es un puntero a una estructura que mantiene informacin sobre el archivo: la direccin del buffer, el cdigo de la operacin que se va a realizar, etc. De nuevo el programador no se debe preocupar de esos detalles: simplemente debe declarar en su programa un puntero a archivo, como ya veremos ms adelante. El modo en que las funciones estndar de ANSI C gestionan todo el acceso a disco es algo transparente al programador. Cmo trabaja realmente el sistema operativo con el archivo sigue siendo algo que no afecta al programador. Pero es necesario que de la misma manera que una funcin de ANSI C ha negociado con el sistema operativo la apertura del archivo y ha facilitado al programador una direccin de memoria, tambin sea una funcin de ANSI C quien cierre al final del proceso los archivos abiertos, de forma tambin transparente para el programador. Si se interrumpe inesperadamente la ejecucin de un programa, o ste termina sin haber cerrado los archivos que tiene

608

Captulo 21. Gestin de archivos.

abiertos, se puede sufrir un dao irreparable sobre esos archivos, y perderlos o perder parte de su informacin. Tambin es transparente al programador el modo en que se accede de hecho a la informacin del archivo. El programa no accede nunca al archivo fsico, sino que acta siempre y nicamente sobre la memoria intermedia o buffer, que es el lugar de almacenamiento temporal de datos. nicamente se almacenan los datos en el archivo fsico cuando la informacin se transfiere desde el buffer hasta el disco. Y esa transferencia no necesariamente coincide con la orden de escritura o lectura que da el programador. De nuevo, por tanto, es muy importante terminar los procesos de acceso a disco de forma regular y normalizada, pues de lo contrario, si la terminacin del programa se realiza de forma anormal, es muy fcil que se pierdan al menos los datos que estaban almacenados en el buffer y que an no haban sido, de hecho, transferidos a disco.

Archivos secuenciales con buffer.


Antes de utilizar un archivo, la primera operacin, previa a cualquier otra, es la de apertura. Ya hemos dicho que cuando abrimos un archivo, la funcin de apertura asignar una direccin para ese archivo. Debe por tanto crearse un puntero para recoger esa direccin. En la biblioteca stdio.h est definido el tipo de dato FILE, que es tipo de dato puntero a archivo. Este puntero nos permite distinguir entre los diferentes ficheros abiertos en el programa. Crea la secuencia o interfaz que nos permite la transferencia de informacin con el archivo apuntado. La sintaxis para la declaracin de un puntero a archivo es la siguiente: FILE *puntero_a_archivo;

609

Informtica Aplicada. Programacin en Lenguaje C.

Vamos ahora a ir viendo diferentes funciones definidas en stdio.h para la manipulacin de archivos.

Apertura de archivo. La funcin fopen abre un archivo y devuelve un puntero asociado al mismo, que puede ser utilizado para que el resto de funciones de manipulacin de archivos accedan a este archivo abierto. Su prototipo es: FILE *fopen(const char*nombre_archivo, const char *modo_apertura); Donde nombre_archivo es el nombre del archivo que se desea abrir. Debe ir entre comillas dobles, como toda cadena de caracteres. El nombre debe estar consignado de tal manera que el sistema operativo sepa identificar el archivo de qu se trata. Y donde modo_apertura es el modo de acceso para el que se abre el archivo. Debe ir en comillas dobles. Los posibles modos de apertura de un archivo secuencial con buffer son: r w Abre un archivo de texto para lectura. El archivo debe existir. Abre un archivo de texto para escritura. Si existe ese archivo, lo borra y lo crea de nuevo. Los datos nuevos se escriben desde el principio. a Abre un archivo de texto para escritura. Los datos nuevos se aaden al final del archivo. Si ese archivo no existe, lo crea. r+ Abre un archivo de texto para lectura/escritura. Los datos se escriben desde el principio. El fichero debe existir.

610

Captulo 21. Gestin de archivos.

w+

Abre un archivo de texto para lectura/escritura. Los datos se escriben desde el principio. Si el fichero no existe, lo crea.

rb wb

Abre un archivo binario para lectura. El archivo debe existir. Abre un archivo binario para escritura. Si existe ese archivo, lo borra y lo crea de nuevo. Los datos nuevos se escriben desde el principio.

ab

Abre un archivo binario para escritura. Los datos nuevos se aaden al final del archivo. Si ese archivo no existe, lo crea.

r+b

Abre un archivo binario para lectura/escritura. Los datos se escriben desde el principio. El fichero debe existir.

w+b

Abre un archivo binario para lectura/escritura. Los datos se escriben desde el principio. Si el fichero no existe, lo crea.

Ya vemos que hay muy diferentes formas de abrir un archivo. Queda claro que de todas ellas destacan dos bloques: aquellas que abren el archivo para manipular una informacin almacenada en binario, y otras que abren el archivo para poder manipularlo en formato texto. Ya iremos viendo ambas formas de trabajar la informacin a medida que vayamos presentando las distintas funciones. La funcin fopen devuelve un puntero a una estructura que recoge las caractersticas del archivo abierto. Si se produce algn error en la apertura del archivo, entonces la funcin fopen devuelve un puntero nulo. Ejemplos simples de esta funcin seran: FILE *fichero; fichero = fopen(datos.dat,w); Que deja abierto el archivo datos.dat para escritura. Si ese archivo ya exista, queda eliminado y se crea otro nuevo y vaco.

611

Informtica Aplicada. Programacin en Lenguaje C.

El nombre del archivo puede introducirse mediante variable: char nombre_archivo[80]; printf(Indique el nombre del archivo ... ); gets(nombre_archivo); fopen(nombre_archivo, w); Y ya hemos dicho que si la funcin fopen no logra abrir el archivo, entonces devuelve un puntero nulo. Es muy conveniente verificar siempre que el fichero ha sido realmente abierto y que no ha habido problemas: FILE *archivo; if(archivo = fopen(datos.dat, w) == NULL) printf(No se puede abrir el archivo\n); Dependiendo del compilador se podrn tener ms o menos archivos abiertos a la vez. En todo caso, siempre se podrn tener, al menos ocho archivos abiertos simultneamente.

Cierre del archivo abierto. La funcin fclose cierra el archivo que ha sido abierto mediante fopen. Su prototipo es el siguiente: int fclose(FILE *nombre_archivo); La funcin devuelve el valor cero si ha cerrado el archivo correctamente. Un error en el cierre de un archivo puede ser fatal y puede generar todo tipo de problemas. El ms grave de ellos es el de la prdida parcial o total de la informacin del archivo. Cuando una funcin termina normalmente su ejecucin, cierra de forma automtica todos sus archivos abiertos. De todas formas es conveniente cerrar los archivos cuando ya no se utilicen dentro de la funcin, y no mantenerlos abiertos en espera de que se finalice su ejecucin.

612

Captulo 21. Gestin de archivos.

Escritura de un carcter en un archivo. Existen dos funciones definidas en stdio.h para escribir un carcter en el archivo. Ambas realizan la misma funcin y ambas se utilizan indistintamente. La duplicidad de definicin es necesaria para preservar la compatibilidad con versiones antiguas de C. Los prototipos de ambas funciones son: int putc(int c, FILE *nombre_archivo); int fputc(int c, FILE * nombre_archivo); Donde nombre_archivo recoge la direccin que ha devuelto la funcin fopen. El archivo debe haber sido abierto para escritura y en formato texto. Y donde la variable c es el carcter que se va a escribir. Por razones histricas, ese carcter se define como un entero, pero de esos dos o cuatro bytes (dependiendo de la longitud de la palabra) slo se toma en consideracin el menos significativo. Si la operacin de escritura se realiza con xito, la funcin devuelve el mismo carcter escrito. Vamos a hacer un programa que solicite al usuario su nombre y entonces guarde ese dato en un archivo que llamaremos nombre.dat. #include <stdio.h> #include <stdlib.h> int main(void) { char nombre[80]; short int i; FILE *archivo; printf("Su nombre ... "); gets(nombre); archivo = fopen("nombre.dat", "w"); if(archivo == NULL) { printf("No se ha podido abrir el archivo.\n"); printf("Pulse una tecla para finalizar... "); getchar(); exit(1);

613

Informtica Aplicada. Programacin en Lenguaje C.

} i = 0; while(nombre[i] != NULL) { fputc(nombre[i],archivo); i++; } fclose(archivo); return 0; } Una vez ejecutado el programa, y si todo ha ido correctamente, se podr abrir el archivo nombre.dat con un editor de texto y comprobar que realmente se ha guardado el nombre en ese archivo.

Lectura de un carcter desde un archivo. De manera anloga a las funciones de escritura, existen tambin funciones de lectura de caracteres desde un archivo. De nuevo hay dos funciones equivalentes, cuyos prototipos son: int fgetc(FILE *nombre_archivo); int getc(FILE * nombre_archivo); Que reciben como parmetro el puntero devuelto por la funcin fopen al abrir el archivo y devuelven el carcter, de nuevo como un entero. El archivo debe haber sido abierto para lectura y en formato texto. Cuando ha llegado al final del archivo, la funcin fgetc, o getc, devuelve una marca de fin de archivo que se codifica como EOF. El cdigo para leer el nombre desde el archivo donde ha quedado almacenado en el programa anterior sera: #include <stdio.h> #include <stdlib.h> int main(void) { char nombre[80]; short int i; FILE *archivo; archivo = fopen("nombre.dat", "r");

614

Captulo 21. Gestin de archivos.

if(archivo == NULL) { printf("No se ha podido abrir el archivo.\n"); printf("Pulse una tecla para finalizar... "); getchar(); exit(1); } i = 0; while((nombre[i++] = fgetc(archivo)) != EOF); /* El ltimo elemento de la cadena ha quedado igual a EOF. Se cambia al carcter fin de cadena, NULL */ nombre[--i] = NULL; fclose(archivo); printf("Su nombre ... %s", nombre); return 0; } Este cdigo mostrar por pantalla el nombre almacenado en el archivo nombre.dat.

Lectura y escritura de una cadena de caracteres. Las funciones fputs y fgets escriben y leen, respectivamente, cadenas de caracteres sobre archivos de disco. Sus prototipos son: int fputs(const char *s, FILE *nombre_archivo); char *fgets(char *s, int n, FILE * nombre_archivo); La funcin fputs escribe la cadena s en el archivo indicado por el puntero nombre_archivo. Si la operacin ha sido correcta, devuelve un valor no negativo. El archivo debe haber sido abierto en formato texto y para escritura o para lectura, dependiendo de la funcin que se emplee. La funcin fgets lee del archivo indicado por el puntero nombre_archivo una cadena de caracteres. Lee los caracteres desde el inicio hasta un total de n, que es el valor que recibe como segundo parmetro. Si antes del carcter n-simo ha terminado la cadena, tambin termina la lectura y cierra la cadena con un carcter nulo.

615

Informtica Aplicada. Programacin en Lenguaje C.

En el programa que vimos para la funcin fputc podramos eliminar la variable i y cambiar la estructura while por la sentencia: fputs(nombre,archivo); Y en el programa que vimos para la funcin fgetc, la sentencia podra quedar sencillamente: fgets(nombre, 80, archivo);

Lectura y escritura formateada. Las funciones fprintf y fscanf de entrada y salida de datos por disco tienen un uso semejante a las funciones printf y scanf, de entrada y salida por consola. Sus prototipos son: int fprintf(FILE *nombre_archivo, const char *cadena_formato [, argumento, ...]); int fscanf(FILE *nombre_archivo, const char *cadena_formato [, direccin, ...]); Donde nombre_archivo es el puntero a archivo que devuelve la funcin fopen. Los dems argumentos de estas dos funciones ya los conocemos, pues son los mismos que las funciones de entrada y salida por consola. La funcin fscanf devuelve el carcter EOF si ha llegado al final del archivo. El archivo debe haber sido abierto en formato texto y para escritura o para lectura, dependiendo de la funcin que se emplee. Veamos un ejemplo de estas dos funciones. Hagamos un programa que guarde en un archivo (que llamaremos numeros.dat) los valores que previamente se han asignado de forma aleatoria a un vector de variables float. Esos valores se almacenan dentro de una cadena de texto. Y luego, el programa vuelve a abrir el archivo para leer los datos y cargarlos en otro vector y los muestra en pantalla.

616

Captulo 21. Gestin de archivos.

#include <stdio.h> #include <stdlib.h> #define TAM 10 int main(void) { float or[TAM], cp[TAM]; short i; FILE *ARCH; char c[100]; randomize(); for(i = 0 ; i < TAM ; i++) or[i] = (float)random(1000) / random(100); ARCH = fopen("numeros.dat", "w"); if(ARCH == NULL) { printf("No se ha podido abrir el archivo.\n"); printf("Pulse una tecla para finalizar... "); getchar(); exit(1); } for(i = 0 ; i < TAM ; i++) fprintf(ARCH,"Valor %04hi-->%12.4f\n",i,or[i]); fclose(ARCH); ARCH = fopen("numeros.dat", "r"); if(ARCH == NULL) { printf("No se ha podido abrir el archivo.\n"); printf("Pulse una tecla para finalizar... "); getchar(); exit(1); } printf("Los valores guardados en el archivo son:\n"); i = 0; while(fscanf(ARCH,"%s%s%s%f",c,c,c,cp + i++)!= EOF); for(i = 0 ; i < TAM ; i++) printf("Valor %04hd --> %12.4f\n",i,cp[i]); fclose(ARCH); return 0; } El archivo contiene (en una ejecucin cualquiera: los valores son aleatorios, y en cada ejecucin llegaremos a valores diferentes) la siguiente informacin:

617

Informtica Aplicada. Programacin en Lenguaje C.

Valor Valor Valor Valor Valor Valor Valor Valor Valor Valor

0000 0001 0002 0003 0004 0005 0006 0007 0008 0009

--> --> --> --> --> --> --> --> --> -->

9.4667 30.4444 12.5821 0.2063 16.4545 28.7308 9.9574 0.1039 18.0000 4.7018

Hemos definido la variable c para que vaya cargando desde el archivo los tramos de cadena de caracteres que no nos interesan para la obtencin, mediante la funcin fscanf, de los sucesivos valores float generados. Con esas tres lecturas de cadena la variable c va leyendo las cadenas Valor; la cadena de caracteres que recoge el ndice i; la cadena-->. La salida por pantalla tendr la misma apariencia que la obtenida en el archivo. Desde luego, con la funcin fscanf es mejor codificar bien la informacin del archivo, porque de lo contrario la lectura de datos desde el archivo puede llegar a hacerse muy incmoda.

Lectura y escritura en archivos binarios. Ya hemos visto las funciones para acceder a los archivos secuenciales de tipo texto. Vamos a ver ahora las funciones de lectura y escritura en forma binaria. Si en todas las funciones anteriores hemos requerido que la apertura del fichero o archivo se hiciera en formato texto, ahora desde luego, para hacer uso de las funciones de escritura y lectura en archivos binarios, el archivo debe hacer sido abierto en formato binario. Las funciones que vamos a ver ahora permiten la lectura o escritura de cualquier tipo de dato. Los prototipos son los siguientes:

618

Captulo 21. Gestin de archivos.

size_t fread(void *buffer, size_t n_bytes, size_t contador, FILE *nombre_archivo); size_t fwrite(const void *buffer, size_t n_bytes, size_t contador, FILE *nombre_archivo); Donde buffer es un puntero a la regin de memoria donde se van a escribir los datos ledos en el archivo, o el lugar donde estn los datos que se desean escribir en el archivo. Habitualmente ser la direccin de una variable. n_bytes es el nmero de bytes que ocupa cada dato que se va a leer o grabar, y contador indica el nmero de datos de ese tamao que se van a leer o grabar. El ltimo parmetro es el de la direccin que devuelve la funcin fopen cuando se abre el archivo. Ambas funciones devuelven el nmero de elementos escritos o ledos. Ese valor debe ser el mismo que el de la variable contador, a menos que haya ocurrido un error. Estas dos funciones son tiles para leer y escribir cualquier tipo de informacin. Es habitual emplearla junto con el operador sizeof, para determinar as la longitud (n_bytes) de cada elemento a leer o escribir. El ejemplo anterior puede servir para ejemplificar ahora el uso de esas dos funciones. El archivo numeros.dat ser ahora de tipo binario. El programa cargar en forma binaria esos valores y luego los leer para calcular el valor medio de todos ellos y mostrarlos por pantalla: #include <stdio.h> #include <stdlib.h> #define TAM 10 int main(void) { float or[TAM], cp[TAM]; double suma = 0; short i; FILE *ARCH; randomize(); for(i = 0 ; i < TAM ; i++) or[i] = (float)random(1000) / random(100); ARCH = fopen("numeros.dat", "wb");

619

Informtica Aplicada. Programacin en Lenguaje C.

if(ARCH == NULL) { printf("No se ha podido abrir el archivo.\n"); printf("Pulse una tecla para finalizar... "); getchar(); exit(1); } fwrite(or,sizeof(float),TAM,ARCH); fclose(ARCH); ARCH = fopen("numeros.dat", "rb"); if(ARCH == NULL) { printf("No se ha podido abrir el archivo.\n"); printf("Pulse una tecla para finalizar... "); getchar(); exit(1); } fread(cp,sizeof(float),TAM,ARCH); fclose(ARCH); for(i = 0 ; i < TAM ; i++) { printf("Valor %04hd --> %12.4f\n",i,cp[i]); suma += *(cp + i); } printf("\n\nLa media es ... %lf", suma / TAM); return 0; }

Otras funciones tiles en el acceso a archivo Funcin feof: Esta funcin (en realidad es una macro) determina el final de archivo. Es conveniente usarla cuando se trabaja con archivos binarios, donde se puede inducir a error y tomar como carcter EOF un valor entero codificado. Su prototipo es: int feof(FILE *nombre_archivo); que devuelve un valor diferente de cero si en la ltima operacin de lectura se ha detectado el valor EOF. en caso contrario devuelve el valor cero.

620

Captulo 21. Gestin de archivos.

Funcin ferror: Esta funcin (en realidad es una macro) determina si se ha producido un error en la ltima operacin sobre el archivo. Su prototipo es: int ferror(FILE * nombre_archivo); Si el valor devuelto es diferente de cero, entonces se ha producido un error; si es igual a cero, entonces no se ha producido error alguno. Si deseamos hacer un programa que controle perfectamente todos los accesos a disco, entonces convendr ejecutar esta funcin despus de cada operacin de lectura o escritura. Funcin remove: Esta funcin elimina un archivo. El archivo ser cerrado si estaba abierto y luego ser eliminado. Quiere esto decir que el archivo quedar destruido, que no es lo mismo que quedarse vaco. Su prototipo es: int remove(const char * nombre_archivo); Donde nombre_archivo es el nombre del archivo que se desea borrar. En ese nombre, como siempre, debe ir bien consignada la ruta completa del archivo. Un archivo as eliminado no es recuperable. Por ejemplo, en nuestro ejemplos anteriores, despus de haber hecho la transferencia de datos al vector de float, podramos ya eliminar el archivo de nuestro disco. Hubiera abastado poner la sentencia: remove("numeros.dat"); Si el archivo no ha podido ser eliminado (por denegacin de permiso o porque el archivo no existe en la ruta y nombre que ha dado el programa) entonces la funcin devuelve el valor -1. Si la operacin de eliminacin del archivo ha sido correcta, entonces devuelve un cero. En realidad, la macro remove lo nico que hace es invocar a la funcin de borrado definida en io.h: la funcin unlink, cuyo prototipo es: int unlink(const char *filename);

621

Informtica Aplicada. Programacin en Lenguaje C.

Y cuyo comportamiento es idntico al explicado para la macro remove.

Entrada y salida sobre archivos de acceso aleatorio.


Disponemos de algunas funciones que permiten acceder de forma aleatoria a una u otra posicin del archivo. Ya dijimos que un archivo, desde el punto de vista del programador es simplemente un puntero a la posicin del archivo (en realidad al buffer) donde va a tener lugar el prximo acceso al archivo. Cuando se abre el archivo ese puntero recoge la direccin de la posicin cero del archivo, es decir, al principio. Cada vez que el programa indica escritura de datos, el puntero termina ubicado al final del archivo. Pero tambin podemos, gracias a algunas funciones definidas en io.h, hacer algunos accesos aleatorios. En realidad, el nico elemento nuevo que se incorpora al hablar de acceso aleatorio es una funcin capaz de posicionar el puntero del archivo devuelto por la funcin fopen en distintas partes del fichero y poder as acceder a datos intermedios. La funcin fseek puede modificar el valor de ese puntero, llevndolo hasta cualquier byte del archivo y logrando as un acceso aleatorio. Es decir, que las funciones estndares de ANSI C logran hacer accesos aleatorios nicamente mediante una funcin que se aade a todas las que ya hemos visto para los accesos secuenciales. El prototipo de la funcin, definida en la biblioteca stdio.h es el siguiente: int fseek(FILE *nombre_archivo, long despl, int modo); Donde nombre_archivo es el puntero que ha devuelto la funcin fopen al abrir el archivo; donde despl es el desplazamiento, en bytes, a efectuar; y donde modo es el punto de referencia que se toma para efectuar el desplazamiento. Para esa definicin de modo, stdio.h define tres constantes diferentes:

622

Captulo 21. Gestin de archivos.

SEEK_SET, que es valor 0. SEEK_CUR, que es valor 1, SEEK_END, que es valor 2. El modo de la funcin fseek puede tomar como valor cualquiera de las tres constantes. Si tiene la primera (SEEK_SET), el desplazamiento se har a partir del inicio del fichero; si tiene la segunda (SEEK_CUR), el desplazamiento se har a partir de la posicin actual del puntero; si tiene la tercera (SEEK_END), el desplazamiento se har a partir del final del fichero. Para la lectura del archivo que habamos visto para ejemplificar la funcin fscanf, las sentencias de lectura quedaran mejor si se hiciera as: printf("Los valores guardados en el archivo son:\n"); i = 0; while(!feof(ARCH)) { fseek(ARCH,16,SEEK_CUR); fscanf(ARCH,"%f",cp + i++); } Donde hemos indicado 16 en el desplazamiento en bytes, porque 16 son los caracteres que no deseamos que se lean en cada lnea. Los desplazamientos en la funcin fseek pueden ser positivos o negativos. Desde luego, si los hacemos desde el principio lo razonable es hacerlos positivos, y si los hacemos desde el final hacerlos negativos. La funcin acepta cualquier desplazamiento y no produce nunca un error. Luego, si el desplazamiento ha sido errneo, y nos hemos posicionado en medio de ninguna parte o en un byte a mitad de dato, entonces la lectura que pueda hacer la funcin que utilicemos ser imprevisible. Una ltima funcin que presentamos en este captulo es la llamada rewind, cuyo prototipo es: void rewind(FILE *nombre_archivo);

623

Informtica Aplicada. Programacin en Lenguaje C.

Que rebobina el archivo, devolviendo el puntero a su posicin inicial, al principio del archivo.

Ejercicios.

21.1.

Descargar desde Internet un archivo con el texto completo de El Quijote. Almacenarlo en formato texto. Darle a este archivo el nombre quijote.txt. Y hacer entonces un programa que vaya leyendo uno a uno los caracteres del archivo y vaya contando cuntas veces aparece cada una de las letras del abecedario. Mostrar al final en pantalla las veces que han aparecido cada una de las letras y tambin el porcentaje de aparicin respecto al total de todas las letras aparecidas.

Vamos a ofrecer dos soluciones a este programa. La primera es la ms trivial: #include <stdio.h> #include <stdlib.h> #include <ctype.h> int main(void) { long letra[27]; short caracter; long suma = 0; short int i; FILE *archivo; for(i = 0 ; i < 26 ; i++) letra[i] = 0; archivo = fopen("quijote.txt", "r"); if(archivo == NULL) { printf("No se ha podido abrir el archivo.\n"); printf("Pulse una tecla para finalizar... "); getchar(); exit(1); }

624

Captulo 21. Gestin de archivos.

while((caracter = fgetc(archivo)) != EOF) { if(isalpha(caracter)) { i = (short)tolower(caracter) - (short)'a'; if(i >= 0 && i < 26) letra[i]++; } } fclose(archivo); for(i = 0 ; i < 26 ; i++) suma += letra[i]; for(i = 0 ; i < 26 ; i++) { printf("[ %c ]= %10ld\t",(char)(i+A),letra[i]); printf("%7.2lf\n",((float)letra[i]/suma)*100); } printf("\n\nTotal letras ... %ld",suma); return 0; } Esta es la solucin primera y sencilla. El vector letras tiene 26 elementos: tantos como letras tiene el abecedario ASCII. Pasamos siempre la letra a minscula porque as no hemos de verificar que nos venga el carcter en mayscula, y nos ahorramos muchas comparaciones. El vector letra se indexa siempre por medio de la variable i. La pega es que con este cdigo no sumamos las veces que aparecen las vocales con acento, o la letra u con diresis. Y, desde luego, no calculamos cuntas veces aparece la letra la letra . Para poder hacer esos clculos, deberemos modificar el programa aadiendo algunas instrucciones: #include <stdio.h> #include <stdlib.h> #include <ctype.h> int main(void) { long letra[27]; short caracter; long suma = 0; short int i; FILE *archivo;

625

Informtica Aplicada. Programacin en Lenguaje C.

for(i = 0 ; i < 27 ; i++) letra[i] = 0; archivo = fopen("quijote.txt", "r"); if(archivo == NULL) { printf("No se ha podido abrir el archivo.\n"); printf("Pulse una tecla para finalizar... "); getchar(); exit(1); } while((caracter = fgetc(archivo)) != EOF) { if(caracter == 209 || caracter == 241) letra[26]++; // letras y else if(caracter == 225 || caracter == 193) letra['a' - 'a']++; // letras y else if(caracter == 233 || caracter == 201) letra['e' - 'a']++; // letras y else if(caracter == 237 || caracter == 205) letra['i' - 'a']++; // letras e else if(caracter == 243 || caracter == 211) letra['o' - 'a']++; // letras y else if(caracter == 250 || caracter == 218) letra['u' - 'a']++; // letras y else if(caracter == 252 || caracter == 220) letra['u' - 'a']++; // letras y else if(isalpha(caracter)) { i = (short)tolower(caracter) - (short)'a'; if(i >= 0 && i < 26) letra[i]++; } } fclose(archivo); for(i = 0 ; i < 27 ; i++) suma += letra[i]; for(i = 0 ; i < 26 ; i++) { printf("[ %c ]= %10ld\t",(char)(i+A),letra[i]); printf("%7.2lf\n",((float)letra[i]/suma)*100); } printf("[ %c ] = %10ld\t", 165,letra[26]); printf("%7.2lf\n",((float)letra[26] / suma) * 100); printf("\n\nTotal letras ... %ld",suma); return 0; }

626

Captulo 21. Gestin de archivos.

21.2.

Implementar una base de datos de asignaturas. El programa ser muy sencillo, y simplemente debe definir una estructura como la que ya estaba definida en un tema anterior. El programa almacenar en disco y aadir al final de archivo cada una de las nuevas asignaturas que se aadan. La informacin se guardar en binario. Se ofrecer la posibilidad de realizar un listado de todas las asignaturas por pantalla o grabando ese listado en disco, creando un documento que se pueda luego tratar con un programa editor de texto.

#include <stdio.h> #include <string.h> #include <stdlib.h> typedef struct { unsigned long clave; char descr[50]; double cred; }asignatura; short mostrar_opciones(void); void error(void); short anyadir(char*); short pantalla(char*); short impresora(char*); int main(void) { char nombre_archivo[80]; short opcion; short oK; printf("Nombre del archivo de asignaturas ... "); gets(nombre_archivo); do { opcion = mostrar_opciones(); switch(opcion) { case '1': oK = anyadir(nombre_archivo);

627

Informtica Aplicada. Programacin en Lenguaje C.

case '2': case '3': case '4': } }while(1); return 0; }

if(oK) error(); break; oK = pantalla(nombre_archivo); if(oK) error(); break; oK = impresora(nombre_archivo); if(oK) error(); break; exit(1);

short mostrar_opciones(void) { char opcion; clrscr(); printf("\n\n\t\tOpciones y Tareas"); printf("\n\n\t1. Aadir nueva asignatura."); printf("\n\t2. Mostrar listado por pantalla."); printf("\n\t3. Mostrar listado en archivo."); printf("\n\t4. Salir del programa."); printf("\n\n\t\t\tElegir opcion ... "); do opcion = getchar(); while(opcion <'0'&&opcion >'4'); return opcion; } void error(void) { printf("Error en la operacion de acceso disco.\n"); printf("Pulse una tecla para terminar ... \n"); getchar(); exit(1); } short anyadir(char archivo[]) { FILE *ARCH; asignatura asig; printf("\n\n\nDATOS DE LA NUEVA ASIGNATURA.\n\n"); printf("clave de la asignatura ... "); scanf( %lu",&asig.clave); printf("\nDescripcion ... "); flushall(); gets(asig.descr); printf("\nCreditos ...... "); scanf( %lf",&asig.cred); ARCH = fopen(archivo,"ab"); fwrite(&asig,sizeof(asig),1,ARCH);

628

Captulo 21. Gestin de archivos.

printf("\n\n\tPulsar una tecla para continuar ... "); getchar(); if(ferror(ARCH)) return 1; fclose(ARCH); return 0; } short pantalla(char archivo[]) { FILE *ARCH; asignatura asig; ARCH = fopen(archivo,"a+b"); rewind(ARCH); while(fread(&asig,sizeof(asig),1,ARCH) == 1) { printf("\n\nClave ......... %lu",asig.clave); printf("\nDescripcion ... %s",asig.descr); printf("\nCreditos ...... %6.1lf",asig.cred); } printf("\n\n\tPulsar una tecla para continuar ... "); getchar(); if(ferror(ARCH)) return 1; fclose(ARCH); return 0; } short impresora(char archivo[]) { FILE *ARCH1, *ARCH2; asignatura asig; ARCH1 = fopen(archivo,"rb"); ARCH2 = fopen("impresora","w"); while(fread(&asig,sizeof(asig),1,ARCH1) == 1) { fprintf(ARCH2,"\n\nClave\t%lu", asig.clave); fprintf(ARCH2,"\nDescripcion \t%s", asig.descr); fprintf(ARCH2,"\nCreditos\t%6.1lf", asig.cred); } printf("\n\n\tPulsar una tecla para continuar ... "); getchar(); if(ferror(ARCH1)) return 1; fclose(ARCH1); fclose(ARCH2); return 0; } La funcin principal presenta nicamente una estructura switch que gestiona cuatro posibles valores para la variable opcion. Esos valores se

629

Informtica Aplicada. Programacin en Lenguaje C.

muestran en la primera de las funciones, la funcin mostrar_opciones, que imprime en pantalla las cuatro posibles opciones del programa, (aadir registros, mostrarlos por pantalla, crear un archivo de impresin, y salir del programa) y devuelve a la funcin principal el valor de la opcin elegida. La funcin anyadir recoge los valores de una nueva asignatura y guarda la informacin, mediante la funcin fwrite, en el archivo que ha indicado el usuario al comenzar la ejecucin del programa. El archivo se abre para aadir y para codificacin binaria: ab. En esta funcin se invoca a otra, llamada flushall. Esta funcin, de la biblioteca stdio.h, vaca todos los buffers de entrada. La ejecutamos antes de la funcin gets para variar el buffer de teclado. A veces ese buffer contiene algn carcter, o el carcter intro pulsado desde la ltima entrada de datos por teclado, y el sistema operativo lo toma como entrada de a funcin gets, que queda ejecutada sin intervencin del usuario. La funcin pantalla muestra por pantalla un listado de las asignaturas introducidas hasta el momento y guardadas en el archivo. Abre el archivo para lectura en formato binario: a+b. No lo hemos abierto como rb para evitar el error en caso de que el usuario quiera leer un archivo inexistente. La funcin impresora hace lo mismo que pantalla, pero en lugar de mostrar los datos por la consola los graba en un archivo de texto. Por eso esa funcin abre dos archivos y va grabando el texto en el archivo abierto como w. Si el archivo impresora ya existe, entonces es eliminado y crea otro en su lugar. Se puede completar el programa con nuevas opciones. Se podra modificar la funcin mostrar_opciones y la funcin main incorporando esas opciones nuevas. Y se crearan las funciones necesarias para esas nuevas tareas. Por ejemplo: eliminar el archivo de asignaturas; hacer una copia de seguridad del archivo; buscar una asignatura en el archivo

630

Captulo 21. Gestin de archivos.

cuya clave sea la que indique el usuario y, si la encuentra, entonces muestre por pantalla la descripcin y el nmero de crditos; etc.

631

Informtica Aplicada. Programacin en Lenguaje C.

632

Potrebbero piacerti anche